/* * Copyright (C) 2007 Eric Seidel <eric@webkit.org> * Copyright (C) 2008 Alp Toker <alp@nuanti.com> * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of * its contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "config.h" #include "DumpRenderTree.h" #include "LayoutTestController.h" #include "WorkQueue.h" #include "WorkQueueItem.h" #include <gtk/gtk.h> #include <webkit/webkit.h> #include <JavaScriptCore/JavaScript.h> #include <wtf/Assertions.h> #include <cassert> #include <getopt.h> #include <stdlib.h> #include <string.h> using namespace std; extern "C" { // This API is not yet public. extern GSList* webkit_web_frame_get_children(WebKitWebFrame* frame); extern gchar* webkit_web_frame_get_inner_text(WebKitWebFrame* frame); extern gchar* webkit_web_frame_dump_render_tree(WebKitWebFrame* frame); } volatile bool done; static bool printSeparators; static int dumpPixels; static int dumpTree = 1; LayoutTestController* gLayoutTestController = 0; static WebKitWebView* webView; WebKitWebFrame* mainFrame = 0; WebKitWebFrame* topLoadingFrame = 0; guint waitToDumpWatchdog = 0; const unsigned maxViewHeight = 600; const unsigned maxViewWidth = 800; static gchar* autocorrectURL(const gchar* url) { if (strncmp("http://", url, 7) != 0 && strncmp("https://", url, 8) != 0) { GString* string = g_string_new("file://"); g_string_append(string, url); return g_string_free(string, FALSE); } return g_strdup(url); } static bool shouldLogFrameLoadDelegates(const char* pathOrURL) { return strstr(pathOrURL, "loading/"); } void dumpFrameScrollPosition(WebKitWebFrame* frame) { } void displayWebView() { } static void appendString(gchar*& target, gchar* string) { gchar* oldString = target; target = g_strconcat(target, string, NULL); g_free(oldString); } static gchar* dumpFramesAsText(WebKitWebFrame* frame) { gchar* result = 0; // Add header for all but the main frame. bool isMainFrame = (webkit_web_view_get_main_frame(webView) == frame); gchar* innerText = webkit_web_frame_get_inner_text(frame); if (isMainFrame) result = g_strdup_printf("%s\n", innerText); else { const gchar* frameName = webkit_web_frame_get_name(frame); result = g_strdup_printf("\n--------\nFrame: '%s'\n--------\n%s\n", frameName, innerText); } g_free(innerText); if (gLayoutTestController->dumpChildFramesAsText()) { GSList* children = webkit_web_frame_get_children(frame); for (GSList* child = children; child; child = g_slist_next(child)) appendString(result, dumpFramesAsText((WebKitWebFrame*)children->data)); g_slist_free(children); } return result; } static void invalidateAnyPreviousWaitToDumpWatchdog() { if (waitToDumpWatchdog) { g_source_remove(waitToDumpWatchdog); waitToDumpWatchdog = 0; } } void dump() { invalidateAnyPreviousWaitToDumpWatchdog(); if (dumpTree) { char* result = 0; bool dumpAsText = gLayoutTestController->dumpAsText(); // FIXME: Also dump text resuls as text. gLayoutTestController->setDumpAsText(dumpAsText); if (gLayoutTestController->dumpAsText()) result = dumpFramesAsText(mainFrame); else { bool isSVGW3CTest = (gLayoutTestController->testPathOrURL().find("svg/W3C-SVG-1.1") != string::npos); GtkAllocation size; size.width = isSVGW3CTest ? 480 : maxViewWidth; size.height = isSVGW3CTest ? 360 : maxViewHeight; gtk_widget_size_allocate(GTK_WIDGET(webView), &size); result = webkit_web_frame_dump_render_tree(mainFrame); } if (!result) { const char* errorMessage; if (gLayoutTestController->dumpAsText()) errorMessage = "[documentElement innerText]"; else if (gLayoutTestController->dumpDOMAsWebArchive()) errorMessage = "[[mainFrame DOMDocument] webArchive]"; else if (gLayoutTestController->dumpSourceAsWebArchive()) errorMessage = "[[mainFrame dataSource] webArchive]"; else errorMessage = "[mainFrame renderTreeAsExternalRepresentation]"; printf("ERROR: nil result from %s", errorMessage); } else { printf("%s", result); g_free(result); if (!gLayoutTestController->dumpAsText() && !gLayoutTestController->dumpDOMAsWebArchive() && !gLayoutTestController->dumpSourceAsWebArchive()) dumpFrameScrollPosition(mainFrame); } if (gLayoutTestController->dumpBackForwardList()) { // FIXME: not implemented } if (printSeparators) { puts("#EOF"); // terminate the content block fputs("#EOF\n", stderr); fflush(stdout); fflush(stderr); } } if (dumpPixels) { if (!gLayoutTestController->dumpAsText() && !gLayoutTestController->dumpDOMAsWebArchive() && !gLayoutTestController->dumpSourceAsWebArchive()) { // FIXME: Add support for dumping pixels } } // FIXME: call displayWebView here when we support --paint puts("#EOF"); // terminate the (possibly empty) pixels block fflush(stdout); fflush(stderr); done = true; } static void setDefaultsToConsistentStateValuesForTesting() { WebKitWebSettings *settings = webkit_web_view_get_settings(webView); g_object_set(G_OBJECT(settings), "default-font-family", "Times", "monospace-font-family", "Courier", "serif-font-family", "Times", "sans-serif-font-family", "Helvetica", "default-font-size", 16, "default-monospace-font-size", 13, "minimum-font-size", 1, NULL); } static void runTest(const string& testPathOrURL) { ASSERT(!testPathOrURL.empty()); // Look for "'" as a separator between the path or URL, and the pixel dump hash that follows. string pathOrURL(testPathOrURL); string expectedPixelHash; size_t separatorPos = pathOrURL.find("'"); if (separatorPos != string::npos) { pathOrURL = string(testPathOrURL, 0, separatorPos); expectedPixelHash = string(testPathOrURL, separatorPos + 1); } gchar* url = autocorrectURL(pathOrURL.c_str()); const string testURL(url); gLayoutTestController = new LayoutTestController(testURL, expectedPixelHash); topLoadingFrame = 0; done = false; if (shouldLogFrameLoadDelegates(pathOrURL.c_str())) gLayoutTestController->setDumpFrameLoadCallbacks(true); WorkQueue::shared()->clear(); WorkQueue::shared()->setFrozen(false); webkit_web_view_open(webView, url); g_free(url); url = NULL; while (!done) g_main_context_iteration(NULL, TRUE); // A blank load seems to be necessary to reset state after certain tests. webkit_web_view_open(webView, "about:blank"); gLayoutTestController->deref(); gLayoutTestController = 0; } void webViewLoadStarted(WebKitWebView* view, WebKitWebFrame* frame, void*) { // Make sure we only set this once per test. If it gets cleared, and then set again, we might // end up doing two dumps for one test. if (!topLoadingFrame && !done) topLoadingFrame = frame; } static gboolean processWork(void* data) { // quit doing work once a load is in progress while (WorkQueue::shared()->count() > 0 && !topLoadingFrame) { WorkQueueItem* item = WorkQueue::shared()->dequeue(); ASSERT(item); item->invoke(); delete item; } // if we didn't start a new load, then we finished all the commands, so we're ready to dump state if (!topLoadingFrame && !gLayoutTestController->waitToDump()) dump(); return FALSE; } static void webViewLoadFinished(WebKitWebView* view, WebKitWebFrame* frame, void*) { if (frame != topLoadingFrame) return; topLoadingFrame = 0; WorkQueue::shared()->setFrozen(true); // first complete load freezes the queue for the rest of this test if (gLayoutTestController->waitToDump()) return; if (WorkQueue::shared()->count()) g_timeout_add(0, processWork, 0); else dump(); } static void webViewWindowObjectCleared(WebKitWebView* view, WebKitWebFrame* frame, JSGlobalContextRef context, JSObjectRef windowObject, gpointer data) { JSValueRef exception = 0; assert(gLayoutTestController); gLayoutTestController->makeWindowObject(context, windowObject, &exception); assert(!exception); } static gboolean webViewConsoleMessage(WebKitWebView* view, const gchar* message, unsigned int line, const gchar* sourceId, gpointer data) { fprintf(stdout, "CONSOLE MESSAGE: line %d: %s\n", line, message); return TRUE; } static gboolean webViewScriptAlert(WebKitWebView* view, WebKitWebFrame* frame, const gchar* message, gpointer data) { fprintf(stdout, "ALERT: %s\n", message); return TRUE; } static gboolean webViewScriptPrompt(WebKitWebView* webView, WebKitWebFrame* frame, const gchar* message, const gchar* defaultValue, gchar** value, gpointer data) { fprintf(stdout, "PROMPT: %s, default text: %s\n", message, defaultValue); *value = g_strdup(defaultValue); return TRUE; } static gboolean webViewScriptConfirm(WebKitWebView* view, WebKitWebFrame* frame, const gchar* message, gboolean* didConfirm, gpointer data) { fprintf(stdout, "CONFIRM: %s\n", message); *didConfirm = TRUE; return TRUE; } static void webViewTitleChanged(WebKitWebView* view, WebKitWebFrame* frame, const gchar* title, gpointer data) { if (gLayoutTestController->dumpTitleChanges() && !done) printf("TITLE CHANGED: %s\n", title ? title : ""); } int main(int argc, char* argv[]) { g_thread_init(NULL); gtk_init(&argc, &argv); struct option options[] = { {"notree", no_argument, &dumpTree, false}, {"pixel-tests", no_argument, &dumpPixels, true}, {"tree", no_argument, &dumpTree, true}, {NULL, 0, NULL, 0} }; int option; while ((option = getopt_long(argc, (char* const*)argv, "", options, NULL)) != -1) switch (option) { case '?': // unknown or ambiguous option case ':': // missing argument exit(1); break; } GtkWidget* window = gtk_window_new(GTK_WINDOW_POPUP); GtkContainer* container = GTK_CONTAINER(gtk_fixed_new()); gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(container)); gtk_widget_realize(window); webView = WEBKIT_WEB_VIEW(webkit_web_view_new()); gtk_container_add(container, GTK_WIDGET(webView)); gtk_widget_realize(GTK_WIDGET(webView)); mainFrame = webkit_web_view_get_main_frame(webView); g_signal_connect(G_OBJECT(webView), "load-started", G_CALLBACK(webViewLoadStarted), 0); g_signal_connect(G_OBJECT(webView), "load-finished", G_CALLBACK(webViewLoadFinished), 0); g_signal_connect(G_OBJECT(webView), "window-object-cleared", G_CALLBACK(webViewWindowObjectCleared), 0); g_signal_connect(G_OBJECT(webView), "console-message", G_CALLBACK(webViewConsoleMessage), 0); g_signal_connect(G_OBJECT(webView), "script-alert", G_CALLBACK(webViewScriptAlert), 0); g_signal_connect(G_OBJECT(webView), "script-prompt", G_CALLBACK(webViewScriptPrompt), 0); g_signal_connect(G_OBJECT(webView), "script-confirm", G_CALLBACK(webViewScriptConfirm), 0); g_signal_connect(G_OBJECT(webView), "title-changed", G_CALLBACK(webViewTitleChanged), 0); setDefaultsToConsistentStateValuesForTesting(); if (argc == optind+1 && strcmp(argv[optind], "-") == 0) { char filenameBuffer[2048]; printSeparators = true; while (fgets(filenameBuffer, sizeof(filenameBuffer), stdin)) { char* newLineCharacter = strchr(filenameBuffer, '\n'); if (newLineCharacter) *newLineCharacter = '\0'; if (strlen(filenameBuffer) == 0) continue; runTest(filenameBuffer); } } else { printSeparators = (optind < argc-1 || (dumpPixels && dumpTree)); for (int i = optind; i != argc; ++i) runTest(argv[i]); } return 0; }