2 * Copyright (C) 2005, 2006 Apple Computer, Inc. All rights reserved.
3 * Copyright (C) 2006 Nikolas Zimmermann <zimmermann@kde.org>
4 * Copyright (C) 2008 Nokia Corporation and/or its subsidiary(-ies)
5 * Copyright (C) 2009 Torch Mobile Inc. http://www.torchmobile.com/
7 * Redistribution and use in source and binary forms, with or without
8 * modification, are permitted provided that the following conditions
11 * 1. Redistributions of source code must retain the above copyright
12 * notice, this list of conditions and the following disclaimer.
13 * 2. Redistributions in binary form must reproduce the above copyright
14 * notice, this list of conditions and the following disclaimer in the
15 * documentation and/or other materials provided with the distribution.
16 * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
17 * its contributors may be used to endorse or promote products derived
18 * from this software without specific prior written permission.
20 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
21 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
22 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
24 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
25 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
26 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
27 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
29 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32 #include "DumpRenderTree.h"
33 #include "EventSenderQt.h"
34 #include "LayoutTestControllerQt.h"
35 #include "TextInputControllerQt.h"
36 #include "jsobjects.h"
37 #include "testplugin.h"
38 #include "WorkQueue.h"
41 #include <QCryptographicHash>
46 #include <QScrollArea>
47 #include <QApplication>
50 #include <QFocusEvent>
51 #include <QFontDatabase>
52 #include <QNetworkAccessManager>
53 #include <QNetworkReply>
54 #include <QNetworkRequest>
58 #include <qwebframe.h>
60 #include <qwebsettings.h>
61 #include <qwebsecurityorigin.h>
64 #include <fontconfig/fontconfig.h>
70 extern void qt_drt_run(bool b);
71 extern void qt_dump_set_accepts_editing(bool b);
72 extern void qt_dump_frame_loader(bool b);
73 extern void qt_drt_clearFrameName(QWebFrame* qFrame);
74 extern void qt_drt_overwritePluginDirectories();
75 extern void qt_drt_resetOriginAccessWhiteLists();
76 extern bool qt_drt_hasDocumentElement(QWebFrame* qFrame);
80 // Choose some default values.
81 const unsigned int maxViewWidth = 800;
82 const unsigned int maxViewHeight = 600;
84 NetworkAccessManager::NetworkAccessManager(QObject* parent)
85 : QNetworkAccessManager(parent)
88 connect(this, SIGNAL(sslErrors(QNetworkReply*, const QList<QSslError>&)),
89 this, SLOT(sslErrorsEncountered(QNetworkReply*, const QList<QSslError>&)));
94 void NetworkAccessManager::sslErrorsEncountered(QNetworkReply* reply, const QList<QSslError>& errors)
96 if (reply->url().host() == "127.0.0.1" || reply->url().host() == "localhost") {
99 // Accept any HTTPS certificate.
100 foreach (const QSslError& error, errors) {
101 if (error.error() < QSslError::UnableToGetIssuerCertificate || error.error() > QSslError::HostNameMismatch) {
108 reply->ignoreSslErrors();
113 WebPage::WebPage(QWidget *parent, DumpRenderTree *drt)
117 QWebSettings* globalSettings = QWebSettings::globalSettings();
119 globalSettings->setFontSize(QWebSettings::MinimumFontSize, 5);
120 globalSettings->setFontSize(QWebSettings::MinimumLogicalFontSize, 5);
121 globalSettings->setFontSize(QWebSettings::DefaultFontSize, 16);
122 globalSettings->setFontSize(QWebSettings::DefaultFixedFontSize, 13);
124 globalSettings->setAttribute(QWebSettings::JavascriptCanOpenWindows, true);
125 globalSettings->setAttribute(QWebSettings::JavascriptCanAccessClipboard, true);
126 globalSettings->setAttribute(QWebSettings::LinksIncludedInFocusChain, false);
127 globalSettings->setAttribute(QWebSettings::PluginsEnabled, true);
128 globalSettings->setAttribute(QWebSettings::LocalContentCanAccessRemoteUrls, true);
129 globalSettings->setAttribute(QWebSettings::JavascriptEnabled, true);
130 globalSettings->setAttribute(QWebSettings::PrivateBrowsingEnabled, false);
131 globalSettings->setAttribute(QWebSettings::OfflineWebApplicationCacheEnabled, false);
133 connect(this, SIGNAL(geometryChangeRequested(const QRect &)),
134 this, SLOT(setViewGeometry(const QRect & )));
136 setNetworkAccessManager(new NetworkAccessManager(this));
137 setPluginFactory(new TestPlugin(this));
140 void WebPage::resetSettings()
142 // After each layout test, reset the settings that may have been changed by
143 // layoutTestController.overridePreference() or similar.
145 settings()->resetFontSize(QWebSettings::DefaultFontSize);
146 settings()->resetAttribute(QWebSettings::JavascriptCanOpenWindows);
147 settings()->resetAttribute(QWebSettings::JavascriptEnabled);
148 settings()->resetAttribute(QWebSettings::PrivateBrowsingEnabled);
149 settings()->resetAttribute(QWebSettings::LinksIncludedInFocusChain);
150 settings()->resetAttribute(QWebSettings::OfflineWebApplicationCacheEnabled);
151 QWebSettings::setMaximumPagesInCache(0); // reset to default
154 QWebPage *WebPage::createWindow(QWebPage::WebWindowType)
156 return m_drt->createWindow();
159 void WebPage::javaScriptAlert(QWebFrame*, const QString& message)
161 if (!isTextOutputEnabled())
164 fprintf(stdout, "ALERT: %s\n", message.toUtf8().constData());
167 static QString urlSuitableForTestResult(const QString& url)
169 if (url.isEmpty() || !url.startsWith(QLatin1String("file://")))
172 return QFileInfo(url).fileName();
175 void WebPage::javaScriptConsoleMessage(const QString& message, int lineNumber, const QString&)
177 if (!isTextOutputEnabled())
181 if (!message.isEmpty()) {
182 newMessage = message;
184 size_t fileProtocol = newMessage.indexOf(QLatin1String("file://"));
185 if (fileProtocol != -1) {
186 newMessage = newMessage.left(fileProtocol) + urlSuitableForTestResult(newMessage.mid(fileProtocol));
190 fprintf (stdout, "CONSOLE MESSAGE: line %d: %s\n", lineNumber, newMessage.toUtf8().constData());
193 bool WebPage::javaScriptConfirm(QWebFrame*, const QString& msg)
195 if (!isTextOutputEnabled())
198 fprintf(stdout, "CONFIRM: %s\n", msg.toUtf8().constData());
202 bool WebPage::javaScriptPrompt(QWebFrame*, const QString& msg, const QString& defaultValue, QString* result)
204 if (!isTextOutputEnabled())
207 fprintf(stdout, "PROMPT: %s, default text: %s\n", msg.toUtf8().constData(), defaultValue.toUtf8().constData());
208 *result = defaultValue;
212 bool WebPage::acceptNavigationRequest(QWebFrame* frame, const QNetworkRequest& request, NavigationType type)
214 if (m_drt->layoutTestController()->waitForPolicy()) {
215 QString url = QString::fromUtf8(request.url().toEncoded());
216 QString typeDescription;
219 case NavigationTypeLinkClicked:
220 typeDescription = "link clicked";
222 case NavigationTypeFormSubmitted:
223 typeDescription = "form submitted";
225 case NavigationTypeBackOrForward:
226 typeDescription = "back/forward";
228 case NavigationTypeReload:
229 typeDescription = "reload";
231 case NavigationTypeFormResubmitted:
232 typeDescription = "form resubmitted";
234 case NavigationTypeOther:
235 typeDescription = "other";
238 typeDescription = "illegal value";
241 if (isTextOutputEnabled())
242 fprintf(stdout, "Policy delegate: attempt to load %s with navigation type '%s'\n",
243 url.toUtf8().constData(), typeDescription.toUtf8().constData());
245 m_drt->layoutTestController()->notifyDone();
247 return QWebPage::acceptNavigationRequest(frame, request, type);
250 bool WebPage::supportsExtension(QWebPage::Extension extension) const
252 if (extension == QWebPage::ErrorPageExtension)
253 return m_drt->layoutTestController()->shouldHandleErrorPages();
258 bool WebPage::extension(Extension extension, const ExtensionOption *option, ExtensionReturn *output)
260 const QWebPage::ErrorPageExtensionOption* info = static_cast<const QWebPage::ErrorPageExtensionOption*>(option);
262 // Lets handle error pages for the main frame for now.
263 if (info->frame != mainFrame())
266 QWebPage::ErrorPageExtensionReturn* errorPage = static_cast<QWebPage::ErrorPageExtensionReturn*>(output);
268 errorPage->content = QString("data:text/html,<body/>").toUtf8();
273 DumpRenderTree::DumpRenderTree()
274 : m_dumpPixels(false)
277 , m_enableTextOutput(false)
279 qt_drt_overwritePluginDirectories();
280 QWebSettings::enablePersistentStorage();
282 // create our primary testing page/view.
283 QWebView *view = new QWebView(0);
284 view->resize(QSize(maxViewWidth, maxViewHeight));
285 m_page = new WebPage(view, this);
286 view->setPage(m_page);
288 // create out controllers. This has to be done before connectFrame,
289 // as it exports there to the JavaScript DOM window.
290 m_controller = new LayoutTestController(this);
291 connect(m_controller, SIGNAL(done()), this, SLOT(dump()));
292 m_eventSender = new EventSender(m_page);
293 m_textInputController = new TextInputController(m_page);
294 m_gcController = new GCController(m_page);
296 // now connect our different signals
297 connect(m_page, SIGNAL(frameCreated(QWebFrame *)),
298 this, SLOT(connectFrame(QWebFrame *)));
299 connectFrame(m_page->mainFrame());
301 connect(m_page, SIGNAL(loadFinished(bool)),
302 m_controller, SLOT(maybeDump(bool)));
304 connect(m_page->mainFrame(), SIGNAL(titleChanged(const QString&)),
305 SLOT(titleChanged(const QString&)));
306 connect(m_page, SIGNAL(databaseQuotaExceeded(QWebFrame*,QString)),
307 this, SLOT(dumpDatabaseQuota(QWebFrame*,QString)));
308 connect(m_page, SIGNAL(statusBarMessage(const QString&)),
309 this, SLOT(statusBarMessage(const QString&)));
311 QObject::connect(this, SIGNAL(quit()), qApp, SLOT(quit()), Qt::QueuedConnection);
313 QFocusEvent event(QEvent::FocusIn, Qt::ActiveWindowFocusReason);
314 QApplication::sendEvent(view, &event);
317 DumpRenderTree::~DumpRenderTree()
325 void DumpRenderTree::open()
329 m_stdin->open(stdin, QFile::ReadOnly);
333 m_notifier = new QSocketNotifier(STDIN_FILENO, QSocketNotifier::Read);
334 connect(m_notifier, SIGNAL(activated(int)), this, SLOT(readStdin(int)));
338 static void clearHistory(QWebPage* page)
340 // QWebHistory::clear() leaves current page, so remove it as well by setting
341 // max item count to 0, and then setting it back to it's original value.
343 QWebHistory* history = page->history();
344 int itemCount = history->maximumItemCount();
347 history->setMaximumItemCount(0);
348 history->setMaximumItemCount(itemCount);
351 void DumpRenderTree::resetToConsistentStateBeforeTesting()
353 // reset so that any current loads are stopped
354 // NOTE: that this has to be done before the layoutTestController is
355 // reset or we get timeouts for some tests.
356 m_page->blockSignals(true);
357 m_page->triggerAction(QWebPage::Stop);
358 m_page->blockSignals(false);
360 // reset the layoutTestController at this point, so that we under no
361 // circumstance dump (stop the waitUntilDone timer) during the reset
363 m_controller->reset();
365 closeRemainingWindows();
367 static_cast<WebPage*>(m_page)->resetSettings();
368 m_page->undoStack()->clear();
369 m_page->mainFrame()->setZoomFactor(1.0);
370 clearHistory(m_page);
371 qt_drt_clearFrameName(m_page->mainFrame());
373 WorkQueue::shared()->clear();
374 WorkQueue::shared()->setFrozen(false);
376 qt_drt_resetOriginAccessWhiteLists();
378 QLocale::setDefault(QLocale::c());
379 setlocale(LC_ALL, "");
382 void DumpRenderTree::open(const QUrl& aurl)
384 resetToConsistentStateBeforeTesting();
387 m_expectedHash = QString();
389 // single quote marks the pixel dump hash
390 QString str = url.toString();
391 int i = str.indexOf('\'');
393 m_expectedHash = str.mid(i + 1, str.length());
394 str.remove(i, str.length());
399 // W3C SVG tests expect to be 480x360
400 bool isW3CTest = url.toString().contains("svg/W3C-SVG-1.1");
401 int width = isW3CTest ? 480 : maxViewWidth;
402 int height = isW3CTest ? 360 : maxViewHeight;
403 m_page->view()->resize(QSize(width, height));
404 m_page->setPreferredContentsSize(QSize());
405 m_page->setViewportSize(QSize(width, height));
407 QFocusEvent ev(QEvent::FocusIn);
410 QFontDatabase::removeAllApplicationFonts();
411 #if defined(Q_WS_X11)
415 qt_dump_frame_loader(url.toString().contains("loading/"));
416 setTextOutputEnabled(true);
417 m_page->mainFrame()->load(url);
420 void DumpRenderTree::readStdin(int /* socket */)
422 // Read incoming data from stdin...
423 QByteArray line = m_stdin->readLine();
424 if (line.endsWith('\n'))
425 line.truncate(line.size()-1);
426 //fprintf(stderr, "\n opening %s\n", line.constData());
430 if (line.startsWith("http:") || line.startsWith("https:"))
434 open(QUrl::fromLocalFile(fi.absoluteFilePath()));
440 void DumpRenderTree::setDumpPixels(bool dump)
445 void DumpRenderTree::closeRemainingWindows()
447 foreach(QWidget *widget, windows)
452 void DumpRenderTree::initJSObjects()
454 QWebFrame *frame = qobject_cast<QWebFrame*>(sender());
456 frame->addToJavaScriptWindowObject(QLatin1String("layoutTestController"), m_controller);
457 frame->addToJavaScriptWindowObject(QLatin1String("eventSender"), m_eventSender);
458 frame->addToJavaScriptWindowObject(QLatin1String("textInputController"), m_textInputController);
459 frame->addToJavaScriptWindowObject(QLatin1String("GCController"), m_gcController);
463 QString DumpRenderTree::dumpFramesAsText(QWebFrame* frame)
465 if (!frame || !qt_drt_hasDocumentElement(frame))
469 QWebFrame *parent = qobject_cast<QWebFrame *>(frame->parent());
471 result.append(QLatin1String("\n--------\nFrame: '"));
472 result.append(frame->frameName());
473 result.append(QLatin1String("'\n--------\n"));
476 QString innerText = frame->toPlainText();
477 result.append(innerText);
478 result.append(QLatin1String("\n"));
480 if (m_controller->shouldDumpChildrenAsText()) {
481 QList<QWebFrame *> children = frame->childFrames();
482 for (int i = 0; i < children.size(); ++i)
483 result += dumpFramesAsText(children.at(i));
489 static QString dumpHistoryItem(const QWebHistoryItem& item, int indent, bool current)
495 result.append(QLatin1String("curr->"));
498 for (int i = start; i < indent; i++)
501 QString url = item.url().toString();
502 if (url.contains("file://")) {
503 static QString layoutTestsString("/LayoutTests/");
504 static QString fileTestString("(file test):");
506 QString res = url.mid(url.indexOf(layoutTestsString) + layoutTestsString.length());
510 result.append(fileTestString);
516 // FIXME: Wrong, need (private?) API for determining this.
517 result.append(QLatin1String(" **nav target**"));
518 result.append(QLatin1String("\n"));
523 QString DumpRenderTree::dumpBackForwardList()
525 QWebHistory* history = webPage()->history();
528 result.append(QLatin1String("\n============== Back Forward List ==============\n"));
531 // " (file test):fast/loader/resources/click-fragment-link.html **nav target**"
532 // "curr-> (file test):fast/loader/resources/click-fragment-link.html#testfragment **nav target**"
534 int maxItems = history->maximumItemCount();
536 foreach (const QWebHistoryItem item, history->backItems(maxItems)) {
539 result.append(dumpHistoryItem(item, 8, false));
542 QWebHistoryItem item = history->currentItem();
544 result.append(dumpHistoryItem(item, 8, true));
546 foreach (const QWebHistoryItem item, history->forwardItems(maxItems)) {
549 result.append(dumpHistoryItem(item, 8, false));
552 result.append(QLatin1String("===============================================\n"));
556 static const char *methodNameStringForFailedTest(LayoutTestController *controller)
558 const char *errorMessage;
559 if (controller->shouldDumpAsText())
560 errorMessage = "[documentElement innerText]";
561 // FIXME: Add when we have support
562 //else if (controller->dumpDOMAsWebArchive())
563 // errorMessage = "[[mainFrame DOMDocument] webArchive]";
564 //else if (controller->dumpSourceAsWebArchive())
565 // errorMessage = "[[mainFrame dataSource] webArchive]";
567 errorMessage = "[mainFrame renderTreeAsExternalRepresentation]";
572 void DumpRenderTree::dump()
574 QWebFrame *mainFrame = m_page->mainFrame();
576 //fprintf(stderr, " Dumping\n");
578 // Dump markup in single file mode...
579 QString markup = mainFrame->toHtml();
580 fprintf(stdout, "Source:\n\n%s\n", markup.toUtf8().constData());
583 // Dump render text...
584 QString resultString;
585 if (m_controller->shouldDumpAsText())
586 resultString = dumpFramesAsText(mainFrame);
588 resultString = mainFrame->renderTreeDump();
590 if (!resultString.isEmpty()) {
591 fprintf(stdout, "%s", resultString.toUtf8().constData());
593 if (m_controller->shouldDumpBackForwardList())
594 fprintf(stdout, "%s", dumpBackForwardList().toUtf8().constData());
597 printf("ERROR: nil result from %s", methodNameStringForFailedTest(m_controller));
599 // signal end of text block
600 fputs("#EOF\n", stdout);
601 fputs("#EOF\n", stderr);
604 QImage image(m_page->viewportSize(), QImage::Format_ARGB32);
605 image.fill(Qt::white);
606 QPainter painter(&image);
607 mainFrame->render(&painter);
610 QCryptographicHash hash(QCryptographicHash::Md5);
611 for (int row = 0; row < image.height(); ++row)
612 hash.addData(reinterpret_cast<const char*>(image.scanLine(row)), image.width() * 4);
613 QString actualHash = hash.result().toHex();
615 fprintf(stdout, "\nActualHash: %s\n", qPrintable(actualHash));
617 bool dumpImage = true;
619 if (!m_expectedHash.isEmpty()) {
620 Q_ASSERT(m_expectedHash.length() == 32);
621 fprintf(stdout, "\nExpectedHash: %s\n", qPrintable(m_expectedHash));
623 if (m_expectedHash == actualHash)
629 buffer.open(QBuffer::WriteOnly);
630 image.save(&buffer, "PNG");
632 const QByteArray &data = buffer.data();
634 printf("Content-Type: %s\n", "image/png");
635 printf("Content-Length: %lu\n", static_cast<unsigned long>(data.length()));
637 const char *ptr = data.data();
638 for(quint32 left = data.length(); left; ) {
639 quint32 block = qMin(left, quint32(1 << 15));
640 quint32 written = fwrite(ptr, 1, block, stdout);
643 if (written == block)
651 puts("#EOF"); // terminate the (possibly empty) pixels block
657 quit(); // Exit now in single file mode...
660 void DumpRenderTree::titleChanged(const QString &s)
662 if (m_controller->shouldDumpTitleChanges())
663 printf("TITLE CHANGED: %s\n", s.toUtf8().data());
666 void DumpRenderTree::connectFrame(QWebFrame *frame)
668 connect(frame, SIGNAL(javaScriptWindowObjectCleared()), this, SLOT(initJSObjects()));
669 connect(frame, SIGNAL(provisionalLoad()),
670 layoutTestController(), SLOT(provisionalLoad()));
673 void DumpRenderTree::dumpDatabaseQuota(QWebFrame* frame, const QString& dbName)
675 if (!m_controller->shouldDumpDatabaseCallbacks())
677 QWebSecurityOrigin origin = frame->securityOrigin();
678 printf("UI DELEGATE DATABASE CALLBACK: exceededDatabaseQuotaForSecurityOrigin:{%s, %s, %i} database:%s\n",
679 origin.scheme().toUtf8().data(),
680 origin.host().toUtf8().data(),
682 dbName.toUtf8().data());
683 origin.setDatabaseQuota(5 * 1024 * 1024);
686 void DumpRenderTree::statusBarMessage(const QString& message)
688 if (!m_controller->shouldDumpStatusCallbacks())
691 printf("UI DELEGATE STATUS CALLBACK: setStatusText:%s\n", message.toUtf8().constData());
694 QWebPage *DumpRenderTree::createWindow()
696 if (!m_controller->canOpenWindows())
698 QWidget *container = new QWidget(0);
699 container->resize(0, 0);
700 container->move(-1, -1);
702 WebPage *page = new WebPage(container, this);
703 connectFrame(page->mainFrame());
704 connect(page, SIGNAL(frameCreated(QWebFrame *)), this, SLOT(connectFrame(QWebFrame *)));
705 windows.append(container);
709 int DumpRenderTree::windowCount() const
712 foreach(QWidget *w, windows) {
713 if (w->children().count())
719 #if defined(Q_WS_X11)
720 void DumpRenderTree::initializeFonts()
722 static int numFonts = -1;
724 // Some test cases may add or remove application fonts (via @font-face).
725 // Make sure to re-initialize the font set if necessary.
726 FcFontSet* appFontSet = FcConfigGetFonts(0, FcSetApplication);
727 if (appFontSet && numFonts >= 0 && appFontSet->nfont == numFonts)
730 QByteArray fontDir = getenv("WEBKIT_TESTFONTS");
731 if (fontDir.isEmpty() || !QDir(fontDir).exists()) {
734 "----------------------------------------------------------------------\n"
735 "WEBKIT_TESTFONTS environment variable is not set correctly.\n"
736 "This variable has to point to the directory containing the fonts\n"
737 "you can clone from git://gitorious.org/qtwebkit/testfonts.git\n"
738 "----------------------------------------------------------------------\n"
742 char currentPath[PATH_MAX+1];
743 if (!getcwd(currentPath, PATH_MAX))
744 qFatal("Couldn't get current working directory");
745 QByteArray configFile = currentPath;
746 FcConfig *config = FcConfigCreate();
747 configFile += "/WebKitTools/DumpRenderTree/qt/fonts.conf";
748 if (!FcConfigParseAndLoad (config, (FcChar8*) configFile.data(), true))
749 qFatal("Couldn't load font configuration file");
750 if (!FcConfigAppFontAddDir (config, (FcChar8*) fontDir.data()))
751 qFatal("Couldn't add font dir!");
752 FcConfigSetCurrent(config);
754 appFontSet = FcConfigGetFonts(config, FcSetApplication);
755 numFonts = appFontSet->nfont;