1/* 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/ 6 * 7 * Redistribution and use in source and binary forms, with or without 8 * modification, are permitted provided that the following conditions 9 * are met: 10 * 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. 19 * 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. 30 */ 31 32#include "config.h" 33 34#include "DumpRenderTreeQt.h" 35#include "DumpRenderTreeSupportQt.h" 36#include "EventSenderQt.h" 37#include "GCControllerQt.h" 38#include "LayoutTestControllerQt.h" 39#include "TextInputControllerQt.h" 40#include "PlainTextControllerQt.h" 41#include "testplugin.h" 42#include "WorkQueue.h" 43 44#include <QApplication> 45#include <QBuffer> 46#include <QCryptographicHash> 47#include <QDir> 48#include <QFile> 49#include <QFileInfo> 50#include <QFocusEvent> 51#include <QFontDatabase> 52#include <QLocale> 53#include <QNetworkAccessManager> 54#include <QNetworkReply> 55#include <QNetworkRequest> 56#include <QPaintDevice> 57#include <QPaintEngine> 58#ifndef QT_NO_PRINTER 59#include <QPrinter> 60#endif 61#include <QUndoStack> 62#include <QUrl> 63 64#include <qwebsettings.h> 65#include <qwebsecurityorigin.h> 66 67#ifndef QT_NO_UITOOLS 68#include <QtUiTools/QUiLoader> 69#endif 70 71#ifdef Q_WS_X11 72#include <fontconfig/fontconfig.h> 73#endif 74 75#include <limits.h> 76#include <locale.h> 77 78#ifndef Q_OS_WIN 79#include <unistd.h> 80#endif 81 82#include <qdebug.h> 83 84namespace WebCore { 85 86const int databaseDefaultQuota = 5 * 1024 * 1024; 87 88NetworkAccessManager::NetworkAccessManager(QObject* parent) 89 : QNetworkAccessManager(parent) 90{ 91#ifndef QT_NO_OPENSSL 92 connect(this, SIGNAL(sslErrors(QNetworkReply*, const QList<QSslError>&)), 93 this, SLOT(sslErrorsEncountered(QNetworkReply*, const QList<QSslError>&))); 94#endif 95} 96 97#ifndef QT_NO_OPENSSL 98void NetworkAccessManager::sslErrorsEncountered(QNetworkReply* reply, const QList<QSslError>& errors) 99{ 100 if (reply->url().host() == "127.0.0.1" || reply->url().host() == "localhost") { 101 bool ignore = true; 102 103 // Accept any HTTPS certificate. 104 foreach (const QSslError& error, errors) { 105 if (error.error() < QSslError::UnableToGetIssuerCertificate || error.error() > QSslError::HostNameMismatch) { 106 ignore = false; 107 break; 108 } 109 } 110 111 if (ignore) 112 reply->ignoreSslErrors(); 113 } 114} 115#endif 116 117 118#ifndef QT_NO_PRINTER 119class NullPrinter : public QPrinter { 120public: 121 class NullPaintEngine : public QPaintEngine { 122 public: 123 virtual bool begin(QPaintDevice*) { return true; } 124 virtual bool end() { return true; } 125 virtual QPaintEngine::Type type() const { return QPaintEngine::User; } 126 virtual void drawPixmap(const QRectF& r, const QPixmap& pm, const QRectF& sr) { } 127 virtual void updateState(const QPaintEngineState& state) { } 128 }; 129 130 virtual QPaintEngine* paintEngine() const { return const_cast<NullPaintEngine*>(&m_engine); } 131 132 NullPaintEngine m_engine; 133}; 134#endif 135 136WebPage::WebPage(QObject* parent, DumpRenderTree* drt) 137 : QWebPage(parent) 138 , m_webInspector(0) 139 , m_drt(drt) 140{ 141 QWebSettings* globalSettings = QWebSettings::globalSettings(); 142 143 globalSettings->setFontSize(QWebSettings::MinimumFontSize, 0); 144 globalSettings->setFontSize(QWebSettings::MinimumLogicalFontSize, 5); 145 globalSettings->setFontSize(QWebSettings::DefaultFontSize, 16); 146 globalSettings->setFontSize(QWebSettings::DefaultFixedFontSize, 13); 147 148 globalSettings->setAttribute(QWebSettings::JavascriptCanOpenWindows, true); 149 globalSettings->setAttribute(QWebSettings::JavascriptCanAccessClipboard, true); 150 globalSettings->setAttribute(QWebSettings::LinksIncludedInFocusChain, false); 151 globalSettings->setAttribute(QWebSettings::PluginsEnabled, true); 152 globalSettings->setAttribute(QWebSettings::LocalContentCanAccessRemoteUrls, true); 153 globalSettings->setAttribute(QWebSettings::JavascriptEnabled, true); 154 globalSettings->setAttribute(QWebSettings::PrivateBrowsingEnabled, false); 155 globalSettings->setAttribute(QWebSettings::SpatialNavigationEnabled, false); 156 157 connect(this, SIGNAL(geometryChangeRequested(const QRect &)), 158 this, SLOT(setViewGeometry(const QRect & ))); 159 160 setNetworkAccessManager(m_drt->networkAccessManager()); 161 setPluginFactory(new TestPlugin(this)); 162 163 connect(this, SIGNAL(featurePermissionRequested(QWebFrame*, QWebPage::Feature)), this, SLOT(requestPermission(QWebFrame*, QWebPage::Feature))); 164 connect(this, SIGNAL(featurePermissionRequestCanceled(QWebFrame*, QWebPage::Feature)), this, SLOT(cancelPermission(QWebFrame*, QWebPage::Feature))); 165} 166 167WebPage::~WebPage() 168{ 169 delete m_webInspector; 170} 171 172QWebInspector* WebPage::webInspector() 173{ 174 if (!m_webInspector) { 175 m_webInspector = new QWebInspector; 176 m_webInspector->setPage(this); 177 } 178 return m_webInspector; 179} 180 181void WebPage::resetSettings() 182{ 183 // After each layout test, reset the settings that may have been changed by 184 // layoutTestController.overridePreference() or similar. 185 settings()->resetFontSize(QWebSettings::DefaultFontSize); 186 settings()->resetAttribute(QWebSettings::JavascriptCanOpenWindows); 187 settings()->resetAttribute(QWebSettings::JavascriptEnabled); 188 settings()->resetAttribute(QWebSettings::PrivateBrowsingEnabled); 189 settings()->resetAttribute(QWebSettings::SpatialNavigationEnabled); 190 settings()->resetAttribute(QWebSettings::LinksIncludedInFocusChain); 191 settings()->resetAttribute(QWebSettings::OfflineWebApplicationCacheEnabled); 192 settings()->resetAttribute(QWebSettings::LocalContentCanAccessRemoteUrls); 193 settings()->resetAttribute(QWebSettings::LocalContentCanAccessFileUrls); 194 settings()->resetAttribute(QWebSettings::PluginsEnabled); 195 settings()->resetAttribute(QWebSettings::JavascriptCanAccessClipboard); 196 settings()->resetAttribute(QWebSettings::AutoLoadImages); 197 198 m_drt->layoutTestController()->setCaretBrowsingEnabled(false); 199 m_drt->layoutTestController()->setFrameFlatteningEnabled(false); 200 m_drt->layoutTestController()->setSmartInsertDeleteEnabled(true); 201 m_drt->layoutTestController()->setSelectTrailingWhitespaceEnabled(false); 202 203 // globalSettings must be reset explicitly. 204 m_drt->layoutTestController()->setXSSAuditorEnabled(false); 205 206 QWebSettings::setMaximumPagesInCache(0); // reset to default 207 settings()->setUserStyleSheetUrl(QUrl()); // reset to default 208 209 DumpRenderTreeSupportQt::setMinimumTimerInterval(this, DumpRenderTreeSupportQt::defaultMinimumTimerInterval()); 210 211 m_pendingGeolocationRequests.clear(); 212} 213 214QWebPage *WebPage::createWindow(QWebPage::WebWindowType) 215{ 216 return m_drt->createWindow(); 217} 218 219void WebPage::javaScriptAlert(QWebFrame*, const QString& message) 220{ 221 if (!isTextOutputEnabled()) 222 return; 223 224 fprintf(stdout, "ALERT: %s\n", message.toUtf8().constData()); 225} 226 227void WebPage::requestPermission(QWebFrame* frame, QWebPage::Feature feature) 228{ 229 switch (feature) { 230 case Notifications: 231 if (!m_drt->layoutTestController()->ignoreReqestForPermission()) 232 setFeaturePermission(frame, feature, PermissionGrantedByUser); 233 break; 234 case Geolocation: 235 if (m_drt->layoutTestController()->isGeolocationPermissionSet()) 236 if (m_drt->layoutTestController()->geolocationPermission()) 237 setFeaturePermission(frame, feature, PermissionGrantedByUser); 238 else 239 setFeaturePermission(frame, feature, PermissionDeniedByUser); 240 else 241 m_pendingGeolocationRequests.append(frame); 242 break; 243 default: 244 break; 245 } 246} 247 248void WebPage::cancelPermission(QWebFrame* frame, QWebPage::Feature feature) 249{ 250 switch (feature) { 251 case Geolocation: 252 m_pendingGeolocationRequests.removeOne(frame); 253 break; 254 default: 255 break; 256 } 257} 258 259void WebPage::permissionSet(QWebPage::Feature feature) 260{ 261 switch (feature) { 262 case Geolocation: 263 { 264 Q_ASSERT(m_drt->layoutTestController()->isGeolocationPermissionSet()); 265 foreach (QWebFrame* frame, m_pendingGeolocationRequests) 266 if (m_drt->layoutTestController()->geolocationPermission()) 267 setFeaturePermission(frame, feature, PermissionGrantedByUser); 268 else 269 setFeaturePermission(frame, feature, PermissionDeniedByUser); 270 271 m_pendingGeolocationRequests.clear(); 272 break; 273 } 274 default: 275 break; 276 } 277} 278 279static QString urlSuitableForTestResult(const QString& url) 280{ 281 if (url.isEmpty() || !url.startsWith(QLatin1String("file://"))) 282 return url; 283 284 return QFileInfo(url).fileName(); 285} 286 287void WebPage::javaScriptConsoleMessage(const QString& message, int lineNumber, const QString&) 288{ 289 if (!isTextOutputEnabled()) 290 return; 291 292 QString newMessage; 293 if (!message.isEmpty()) { 294 newMessage = message; 295 296 size_t fileProtocol = newMessage.indexOf(QLatin1String("file://")); 297 if (fileProtocol != -1) { 298 newMessage = newMessage.left(fileProtocol) + urlSuitableForTestResult(newMessage.mid(fileProtocol)); 299 } 300 } 301 302 fprintf (stdout, "CONSOLE MESSAGE: line %d: %s\n", lineNumber, newMessage.toUtf8().constData()); 303} 304 305bool WebPage::javaScriptConfirm(QWebFrame*, const QString& msg) 306{ 307 if (!isTextOutputEnabled()) 308 return true; 309 310 fprintf(stdout, "CONFIRM: %s\n", msg.toUtf8().constData()); 311 return true; 312} 313 314bool WebPage::javaScriptPrompt(QWebFrame*, const QString& msg, const QString& defaultValue, QString* result) 315{ 316 if (!isTextOutputEnabled()) 317 return true; 318 319 fprintf(stdout, "PROMPT: %s, default text: %s\n", msg.toUtf8().constData(), defaultValue.toUtf8().constData()); 320 *result = defaultValue; 321 return true; 322} 323 324bool WebPage::acceptNavigationRequest(QWebFrame* frame, const QNetworkRequest& request, NavigationType type) 325{ 326 if (m_drt->layoutTestController()->waitForPolicy()) { 327 QString url = QString::fromUtf8(request.url().toEncoded()); 328 QString typeDescription; 329 330 switch (type) { 331 case NavigationTypeLinkClicked: 332 typeDescription = "link clicked"; 333 break; 334 case NavigationTypeFormSubmitted: 335 typeDescription = "form submitted"; 336 break; 337 case NavigationTypeBackOrForward: 338 typeDescription = "back/forward"; 339 break; 340 case NavigationTypeReload: 341 typeDescription = "reload"; 342 break; 343 case NavigationTypeFormResubmitted: 344 typeDescription = "form resubmitted"; 345 break; 346 case NavigationTypeOther: 347 typeDescription = "other"; 348 break; 349 default: 350 typeDescription = "illegal value"; 351 } 352 353 if (isTextOutputEnabled()) 354 fprintf(stdout, "Policy delegate: attempt to load %s with navigation type '%s'\n", 355 url.toUtf8().constData(), typeDescription.toUtf8().constData()); 356 357 m_drt->layoutTestController()->notifyDone(); 358 } 359 return QWebPage::acceptNavigationRequest(frame, request, type); 360} 361 362bool WebPage::supportsExtension(QWebPage::Extension extension) const 363{ 364 if (extension == QWebPage::ErrorPageExtension) 365 return m_drt->layoutTestController()->shouldHandleErrorPages(); 366 367 return false; 368} 369 370bool WebPage::extension(Extension extension, const ExtensionOption *option, ExtensionReturn *output) 371{ 372 const QWebPage::ErrorPageExtensionOption* info = static_cast<const QWebPage::ErrorPageExtensionOption*>(option); 373 374 // Lets handle error pages for the main frame for now. 375 if (info->frame != mainFrame()) 376 return false; 377 378 QWebPage::ErrorPageExtensionReturn* errorPage = static_cast<QWebPage::ErrorPageExtensionReturn*>(output); 379 380 errorPage->content = QString("data:text/html,<body/>").toUtf8(); 381 382 return true; 383} 384 385QObject* WebPage::createPlugin(const QString& classId, const QUrl& url, const QStringList& paramNames, const QStringList& paramValues) 386{ 387 Q_UNUSED(url); 388 Q_UNUSED(paramNames); 389 Q_UNUSED(paramValues); 390#ifndef QT_NO_UITOOLS 391 QUiLoader loader; 392 return loader.createWidget(classId, view()); 393#else 394 Q_UNUSED(classId); 395 return 0; 396#endif 397} 398 399void WebPage::setViewGeometry(const QRect& rect) 400{ 401 if (WebViewGraphicsBased* v = qobject_cast<WebViewGraphicsBased*>(view())) 402 v->scene()->setSceneRect(QRectF(rect)); 403 else if (QWidget *v = view()) 404 v->setGeometry(rect); 405} 406 407WebViewGraphicsBased::WebViewGraphicsBased(QWidget* parent) 408 : m_item(new QGraphicsWebView) 409{ 410 setScene(new QGraphicsScene(this)); 411 scene()->addItem(m_item); 412} 413 414DumpRenderTree::DumpRenderTree() 415 : m_dumpPixels(false) 416 , m_stdin(0) 417 , m_enableTextOutput(false) 418 , m_standAloneMode(false) 419 , m_graphicsBased(false) 420 , m_persistentStoragePath(QString(getenv("DUMPRENDERTREE_TEMP"))) 421{ 422 QByteArray viewMode = getenv("QT_DRT_WEBVIEW_MODE"); 423 if (viewMode == "graphics") 424 setGraphicsBased(true); 425 426 // Set running in DRT mode for qwebpage to create testable objects. 427 DumpRenderTreeSupportQt::setDumpRenderTreeModeEnabled(true); 428 DumpRenderTreeSupportQt::overwritePluginDirectories(); 429 DumpRenderTreeSupportQt::activeMockDeviceOrientationClient(true); 430 QWebSettings::enablePersistentStorage(m_persistentStoragePath); 431 432 m_networkAccessManager = new NetworkAccessManager(this); 433 // create our primary testing page/view. 434 if (isGraphicsBased()) { 435 WebViewGraphicsBased* view = new WebViewGraphicsBased(0); 436 m_page = new WebPage(view, this); 437 view->setPage(m_page); 438 m_mainView = view; 439 } else { 440 QWebView* view = new QWebView(0); 441 m_page = new WebPage(view, this); 442 view->setPage(m_page); 443 m_mainView = view; 444 } 445 // Use a frame group name for all pages created by DumpRenderTree to allow 446 // testing of cross-page frame lookup. 447 DumpRenderTreeSupportQt::webPageSetGroupName(m_page, "org.webkit.qt.DumpRenderTree"); 448 449 m_mainView->setContextMenuPolicy(Qt::NoContextMenu); 450 m_mainView->resize(QSize(LayoutTestController::maxViewWidth, LayoutTestController::maxViewHeight)); 451 452 // clean up cache by resetting quota. 453 qint64 quota = webPage()->settings()->offlineWebApplicationCacheQuota(); 454 webPage()->settings()->setOfflineWebApplicationCacheQuota(quota); 455 456 // create our controllers. This has to be done before connectFrame, 457 // as it exports there to the JavaScript DOM window. 458 m_controller = new LayoutTestController(this); 459 connect(m_controller, SIGNAL(showPage()), this, SLOT(showPage())); 460 connect(m_controller, SIGNAL(hidePage()), this, SLOT(hidePage())); 461 462 // async geolocation permission set by controller 463 connect(m_controller, SIGNAL(geolocationPermissionSet()), this, SLOT(geolocationPermissionSet())); 464 465 connect(m_controller, SIGNAL(done()), this, SLOT(dump())); 466 m_eventSender = new EventSender(m_page); 467 m_textInputController = new TextInputController(m_page); 468 m_plainTextController = new PlainTextController(m_page); 469 m_gcController = new GCController(m_page); 470 471 // now connect our different signals 472 connect(m_page, SIGNAL(frameCreated(QWebFrame *)), 473 this, SLOT(connectFrame(QWebFrame *))); 474 connectFrame(m_page->mainFrame()); 475 476 connect(m_page, SIGNAL(loadFinished(bool)), 477 m_controller, SLOT(maybeDump(bool))); 478 // We need to connect to loadStarted() because notifyDone should only 479 // dump results itself when the last page loaded in the test has finished loading. 480 connect(m_page, SIGNAL(loadStarted()), 481 m_controller, SLOT(resetLoadFinished())); 482 connect(m_page, SIGNAL(windowCloseRequested()), this, SLOT(windowCloseRequested())); 483 connect(m_page, SIGNAL(printRequested(QWebFrame*)), this, SLOT(dryRunPrint(QWebFrame*))); 484 485 connect(m_page->mainFrame(), SIGNAL(titleChanged(const QString&)), 486 SLOT(titleChanged(const QString&))); 487 connect(m_page, SIGNAL(databaseQuotaExceeded(QWebFrame*,QString)), 488 this, SLOT(dumpDatabaseQuota(QWebFrame*,QString))); 489 connect(m_page, SIGNAL(applicationCacheQuotaExceeded(QWebSecurityOrigin *, quint64)), 490 this, SLOT(dumpApplicationCacheQuota(QWebSecurityOrigin *, quint64))); 491 connect(m_page, SIGNAL(statusBarMessage(const QString&)), 492 this, SLOT(statusBarMessage(const QString&))); 493 494 QObject::connect(this, SIGNAL(quit()), qApp, SLOT(quit()), Qt::QueuedConnection); 495 496 DumpRenderTreeSupportQt::setDumpRenderTreeModeEnabled(true); 497 QFocusEvent event(QEvent::FocusIn, Qt::ActiveWindowFocusReason); 498 QApplication::sendEvent(m_mainView, &event); 499} 500 501DumpRenderTree::~DumpRenderTree() 502{ 503 if (!m_redirectOutputFileName.isEmpty()) 504 fclose(stdout); 505 if (!m_redirectErrorFileName.isEmpty()) 506 fclose(stderr); 507 delete m_mainView; 508 delete m_stdin; 509 DumpRenderTreeSupportQt::removeMockDeviceOrientation(); 510} 511 512static void clearHistory(QWebPage* page) 513{ 514 // QWebHistory::clear() leaves current page, so remove it as well by setting 515 // max item count to 0, and then setting it back to it's original value. 516 517 QWebHistory* history = page->history(); 518 int itemCount = history->maximumItemCount(); 519 520 history->clear(); 521 history->setMaximumItemCount(0); 522 history->setMaximumItemCount(itemCount); 523} 524 525void DumpRenderTree::dryRunPrint(QWebFrame* frame) 526{ 527#ifndef QT_NO_PRINTER 528 NullPrinter printer; 529 frame->print(&printer); 530#endif 531} 532 533void DumpRenderTree::resetToConsistentStateBeforeTesting(const QUrl& url) 534{ 535 // reset so that any current loads are stopped 536 // NOTE: that this has to be done before the layoutTestController is 537 // reset or we get timeouts for some tests. 538 m_page->blockSignals(true); 539 m_page->triggerAction(QWebPage::Stop); 540 m_page->blockSignals(false); 541 542 QList<QWebSecurityOrigin> knownOrigins = QWebSecurityOrigin::allOrigins(); 543 for (int i = 0; i < knownOrigins.size(); ++i) 544 knownOrigins[i].setDatabaseQuota(databaseDefaultQuota); 545 546 // reset the layoutTestController at this point, so that we under no 547 // circumstance dump (stop the waitUntilDone timer) during the reset 548 // of the DRT. 549 m_controller->reset(); 550 551 // reset mouse clicks counter 552 m_eventSender->resetClickCount(); 553 554 closeRemainingWindows(); 555 556 m_page->resetSettings(); 557#ifndef QT_NO_UNDOSTACK 558 m_page->undoStack()->clear(); 559#endif 560 m_page->mainFrame()->setZoomFactor(1.0); 561 clearHistory(m_page); 562 DumpRenderTreeSupportQt::clearFrameName(m_page->mainFrame()); 563 564 m_page->mainFrame()->setScrollBarPolicy(Qt::Vertical, Qt::ScrollBarAsNeeded); 565 m_page->mainFrame()->setScrollBarPolicy(Qt::Horizontal, Qt::ScrollBarAsNeeded); 566 567 if (url.scheme() == "http" || url.scheme() == "https") { 568 // credentials may exist from previous tests. 569 m_page->setNetworkAccessManager(0); 570 delete m_networkAccessManager; 571 m_networkAccessManager = new NetworkAccessManager(this); 572 m_page->setNetworkAccessManager(m_networkAccessManager); 573 } 574 575 WorkQueue::shared()->clear(); 576 WorkQueue::shared()->setFrozen(false); 577 578 DumpRenderTreeSupportQt::resetOriginAccessWhiteLists(); 579 580 // Qt defaults to Windows editing behavior. 581 DumpRenderTreeSupportQt::setEditingBehavior(m_page, "win"); 582 583 QLocale::setDefault(QLocale::c()); 584 585 layoutTestController()->setDeveloperExtrasEnabled(true); 586#ifndef Q_OS_WINCE 587 setlocale(LC_ALL, ""); 588#endif 589 590 DumpRenderTreeSupportQt::clearOpener(m_page->mainFrame()); 591} 592 593static bool isGlobalHistoryTest(const QUrl& url) 594{ 595 if (url.path().contains("globalhistory/")) 596 return true; 597 return false; 598} 599 600static bool isWebInspectorTest(const QUrl& url) 601{ 602 if (url.path().contains("inspector/")) 603 return true; 604 return false; 605} 606 607static bool isDumpAsTextTest(const QUrl& url) 608{ 609 if (url.path().contains("dumpAsText/")) 610 return true; 611 return false; 612} 613 614 615void DumpRenderTree::open(const QUrl& url) 616{ 617 DumpRenderTreeSupportQt::dumpResourceLoadCallbacksPath(QFileInfo(url.toString()).path()); 618 resetToConsistentStateBeforeTesting(url); 619 620 if (isWebInspectorTest(m_page->mainFrame()->url())) 621 layoutTestController()->closeWebInspector(); 622 623 if (isWebInspectorTest(url)) 624 layoutTestController()->showWebInspector(); 625 626 if (isDumpAsTextTest(url)) { 627 layoutTestController()->dumpAsText(); 628 setDumpPixels(false); 629 } 630 631 if (isGlobalHistoryTest(url)) 632 layoutTestController()->dumpHistoryCallbacks(); 633 634 // W3C SVG tests expect to be 480x360 635 bool isW3CTest = url.toString().contains("svg/W3C-SVG-1.1"); 636 int width = isW3CTest ? 480 : LayoutTestController::maxViewWidth; 637 int height = isW3CTest ? 360 : LayoutTestController::maxViewHeight; 638 m_mainView->resize(QSize(width, height)); 639 m_page->setPreferredContentsSize(QSize()); 640 m_page->setViewportSize(QSize(width, height)); 641 642 QFocusEvent ev(QEvent::FocusIn); 643 m_page->event(&ev); 644 645 QWebSettings::clearMemoryCaches(); 646#if !(defined(Q_OS_SYMBIAN) && QT_VERSION <= QT_VERSION_CHECK(4, 6, 2)) 647 QFontDatabase::removeAllApplicationFonts(); 648#endif 649#if defined(Q_WS_X11) 650 initializeFonts(); 651#endif 652 653 DumpRenderTreeSupportQt::dumpFrameLoader(url.toString().contains("loading/")); 654 setTextOutputEnabled(true); 655 m_page->mainFrame()->load(url); 656} 657 658void DumpRenderTree::readLine() 659{ 660 if (!m_stdin) { 661 m_stdin = new QFile; 662 m_stdin->open(stdin, QFile::ReadOnly); 663 664 if (!m_stdin->isReadable()) { 665 emit quit(); 666 return; 667 } 668 } 669 670 QByteArray line = m_stdin->readLine().trimmed(); 671 672 if (line.isEmpty()) { 673 emit quit(); 674 return; 675 } 676 677 processLine(QString::fromLocal8Bit(line.constData(), line.length())); 678} 679 680void DumpRenderTree::processArgsLine(const QStringList &args) 681{ 682 setStandAloneMode(true); 683 684 m_standAloneModeTestList = args; 685 686 QFileInfo firstEntry(m_standAloneModeTestList.first()); 687 if (firstEntry.isDir()) { 688 QDir folderEntry(m_standAloneModeTestList.first()); 689 QStringList supportedExt; 690 // Check for all supported extensions (from Scripts/webkitpy/layout_tests/layout_package/test_files.py). 691 supportedExt << "*.html" << "*.shtml" << "*.xml" << "*.xhtml" << "*.xhtmlmp" << "*.pl" << "*.php" << "*.svg"; 692 m_standAloneModeTestList = folderEntry.entryList(supportedExt, QDir::Files); 693 for (int i = 0; i < m_standAloneModeTestList.size(); ++i) 694 m_standAloneModeTestList[i] = folderEntry.absoluteFilePath(m_standAloneModeTestList[i]); 695 } 696 connect(this, SIGNAL(ready()), this, SLOT(loadNextTestInStandAloneMode())); 697 698 if (!m_standAloneModeTestList.isEmpty()) { 699 QString first = m_standAloneModeTestList.takeFirst(); 700 processLine(first); 701 } 702} 703 704void DumpRenderTree::loadNextTestInStandAloneMode() 705{ 706 if (m_standAloneModeTestList.isEmpty()) { 707 emit quit(); 708 return; 709 } 710 QString first = m_standAloneModeTestList.takeFirst(); 711 processLine(first); 712} 713 714void DumpRenderTree::processLine(const QString &input) 715{ 716 QString line = input; 717 718 m_expectedHash = QString(); 719 if (m_dumpPixels) { 720 // single quote marks the pixel dump hash 721 int i = line.indexOf('\''); 722 if (i > -1) { 723 m_expectedHash = line.mid(i + 1, line.length()); 724 line.remove(i, line.length()); 725 } 726 } 727 728 if (line.startsWith(QLatin1String("http:")) 729 || line.startsWith(QLatin1String("https:")) 730 || line.startsWith(QLatin1String("file:"))) { 731 open(QUrl(line)); 732 } else { 733 QFileInfo fi(line); 734 735 if (!fi.exists()) { 736 QDir currentDir = QDir::currentPath(); 737 738 // Try to be smart about where the test is located 739 if (currentDir.dirName() == QLatin1String("LayoutTests")) 740 fi = QFileInfo(currentDir, line.replace(QRegExp(".*?LayoutTests/(.*)"), "\\1")); 741 else if (!line.contains(QLatin1String("LayoutTests"))) 742 fi = QFileInfo(currentDir, line.prepend(QLatin1String("LayoutTests/"))); 743 744 if (!fi.exists()) { 745 emit ready(); 746 return; 747 } 748 } 749 750 open(QUrl::fromLocalFile(fi.absoluteFilePath())); 751 } 752 753 fflush(stdout); 754} 755 756void DumpRenderTree::setDumpPixels(bool dump) 757{ 758 m_dumpPixels = dump; 759} 760 761void DumpRenderTree::closeRemainingWindows() 762{ 763 foreach (QObject* widget, windows) 764 delete widget; 765 windows.clear(); 766} 767 768void DumpRenderTree::initJSObjects() 769{ 770 QWebFrame *frame = qobject_cast<QWebFrame*>(sender()); 771 Q_ASSERT(frame); 772 frame->addToJavaScriptWindowObject(QLatin1String("layoutTestController"), m_controller); 773 frame->addToJavaScriptWindowObject(QLatin1String("eventSender"), m_eventSender); 774 frame->addToJavaScriptWindowObject(QLatin1String("textInputController"), m_textInputController); 775 frame->addToJavaScriptWindowObject(QLatin1String("GCController"), m_gcController); 776 frame->addToJavaScriptWindowObject(QLatin1String("plainText"), m_plainTextController); 777} 778 779void DumpRenderTree::showPage() 780{ 781 m_mainView->show(); 782 // we need a paint event but cannot process all the events 783 QPixmap pixmap(m_mainView->size()); 784 m_mainView->render(&pixmap); 785} 786 787void DumpRenderTree::hidePage() 788{ 789 m_mainView->hide(); 790} 791 792QString DumpRenderTree::dumpFrameScrollPosition(QWebFrame* frame) 793{ 794 if (!frame || !DumpRenderTreeSupportQt::hasDocumentElement(frame)) 795 return QString(); 796 797 QString result; 798 QPoint pos = frame->scrollPosition(); 799 if (pos.x() > 0 || pos.y() > 0) { 800 QWebFrame* parent = qobject_cast<QWebFrame *>(frame->parent()); 801 if (parent) 802 result.append(QString("frame '%1' ").arg(frame->title())); 803 result.append(QString("scrolled to %1,%2\n").arg(pos.x()).arg(pos.y())); 804 } 805 806 if (m_controller->shouldDumpChildFrameScrollPositions()) { 807 QList<QWebFrame*> children = frame->childFrames(); 808 for (int i = 0; i < children.size(); ++i) 809 result += dumpFrameScrollPosition(children.at(i)); 810 } 811 return result; 812} 813 814QString DumpRenderTree::dumpFramesAsText(QWebFrame* frame) 815{ 816 if (!frame || !DumpRenderTreeSupportQt::hasDocumentElement(frame)) 817 return QString(); 818 819 QString result; 820 QWebFrame* parent = qobject_cast<QWebFrame*>(frame->parent()); 821 if (parent) { 822 result.append(QLatin1String("\n--------\nFrame: '")); 823 result.append(frame->frameName()); 824 result.append(QLatin1String("'\n--------\n")); 825 } 826 827 QString innerText = frame->toPlainText(); 828 result.append(innerText); 829 result.append(QLatin1String("\n")); 830 831 if (m_controller->shouldDumpChildrenAsText()) { 832 QList<QWebFrame *> children = frame->childFrames(); 833 for (int i = 0; i < children.size(); ++i) 834 result += dumpFramesAsText(children.at(i)); 835 } 836 837 return result; 838} 839 840static QString dumpHistoryItem(const QWebHistoryItem& item, int indent, bool current) 841{ 842 QString result; 843 844 int start = 0; 845 if (current) { 846 result.append(QLatin1String("curr->")); 847 start = 6; 848 } 849 for (int i = start; i < indent; i++) 850 result.append(' '); 851 852 QString url = item.url().toEncoded(); 853 if (url.contains("file://")) { 854 static QString layoutTestsString("/LayoutTests/"); 855 static QString fileTestString("(file test):"); 856 857 QString res = url.mid(url.indexOf(layoutTestsString) + layoutTestsString.length()); 858 if (res.isEmpty()) 859 return result; 860 861 result.append(fileTestString); 862 result.append(res); 863 } else { 864 result.append(url); 865 } 866 867 QString target = DumpRenderTreeSupportQt::historyItemTarget(item); 868 if (!target.isEmpty()) 869 result.append(QString(QLatin1String(" (in frame \"%1\")")).arg(target)); 870 871 if (DumpRenderTreeSupportQt::isTargetItem(item)) 872 result.append(QLatin1String(" **nav target**")); 873 result.append(QLatin1String("\n")); 874 875 QMap<QString, QWebHistoryItem> children = DumpRenderTreeSupportQt::getChildHistoryItems(item); 876 foreach (QWebHistoryItem item, children) 877 result += dumpHistoryItem(item, 12, false); 878 879 return result; 880} 881 882QString DumpRenderTree::dumpBackForwardList(QWebPage* page) 883{ 884 QWebHistory* history = page->history(); 885 886 QString result; 887 result.append(QLatin1String("\n============== Back Forward List ==============\n")); 888 889 // FORMAT: 890 // " (file test):fast/loader/resources/click-fragment-link.html **nav target**" 891 // "curr-> (file test):fast/loader/resources/click-fragment-link.html#testfragment **nav target**" 892 893 int maxItems = history->maximumItemCount(); 894 895 foreach (const QWebHistoryItem item, history->backItems(maxItems)) { 896 if (!item.isValid()) 897 continue; 898 result.append(dumpHistoryItem(item, 8, false)); 899 } 900 901 QWebHistoryItem item = history->currentItem(); 902 if (item.isValid()) 903 result.append(dumpHistoryItem(item, 8, true)); 904 905 foreach (const QWebHistoryItem item, history->forwardItems(maxItems)) { 906 if (!item.isValid()) 907 continue; 908 result.append(dumpHistoryItem(item, 8, false)); 909 } 910 911 result.append(QLatin1String("===============================================\n")); 912 return result; 913} 914 915static const char *methodNameStringForFailedTest(LayoutTestController *controller) 916{ 917 const char *errorMessage; 918 if (controller->shouldDumpAsText()) 919 errorMessage = "[documentElement innerText]"; 920 // FIXME: Add when we have support 921 //else if (controller->dumpDOMAsWebArchive()) 922 // errorMessage = "[[mainFrame DOMDocument] webArchive]"; 923 //else if (controller->dumpSourceAsWebArchive()) 924 // errorMessage = "[[mainFrame dataSource] webArchive]"; 925 else 926 errorMessage = "[mainFrame renderTreeAsExternalRepresentation]"; 927 928 return errorMessage; 929} 930 931void DumpRenderTree::dump() 932{ 933 // Prevent any further frame load or resource load callbacks from appearing after we dump the result. 934 DumpRenderTreeSupportQt::dumpFrameLoader(false); 935 DumpRenderTreeSupportQt::dumpResourceLoadCallbacks(false); 936 937 QWebFrame *mainFrame = m_page->mainFrame(); 938 939 if (isStandAloneMode()) { 940 QString markup = mainFrame->toHtml(); 941 fprintf(stdout, "Source:\n\n%s\n", markup.toUtf8().constData()); 942 } 943 944 QString mimeType = DumpRenderTreeSupportQt::responseMimeType(mainFrame); 945 if (mimeType == "text/plain") 946 m_controller->dumpAsText(); 947 948 // Dump render text... 949 QString resultString; 950 if (m_controller->shouldDumpAsText()) 951 resultString = dumpFramesAsText(mainFrame); 952 else { 953 resultString = mainFrame->renderTreeDump(); 954 resultString += dumpFrameScrollPosition(mainFrame); 955 } 956 if (!resultString.isEmpty()) { 957 fprintf(stdout, "Content-Type: text/plain\n"); 958 fprintf(stdout, "%s", resultString.toUtf8().constData()); 959 960 if (m_controller->shouldDumpBackForwardList()) { 961 fprintf(stdout, "%s", dumpBackForwardList(webPage()).toUtf8().constData()); 962 foreach (QObject* widget, windows) { 963 QWebPage* page = qobject_cast<QWebPage*>(widget->findChild<QWebPage*>()); 964 fprintf(stdout, "%s", dumpBackForwardList(page).toUtf8().constData()); 965 } 966 } 967 968 } else 969 printf("ERROR: nil result from %s", methodNameStringForFailedTest(m_controller)); 970 971 // signal end of text block 972 fputs("#EOF\n", stdout); 973 fputs("#EOF\n", stderr); 974 975 // FIXME: All other ports don't dump pixels, if generatePixelResults is false. 976 if (m_dumpPixels) { 977 QImage image(m_page->viewportSize(), QImage::Format_ARGB32); 978 image.fill(Qt::white); 979 QPainter painter(&image); 980 mainFrame->render(&painter); 981 painter.end(); 982 983 QCryptographicHash hash(QCryptographicHash::Md5); 984 for (int row = 0; row < image.height(); ++row) 985 hash.addData(reinterpret_cast<const char*>(image.scanLine(row)), image.width() * 4); 986 QString actualHash = hash.result().toHex(); 987 988 fprintf(stdout, "\nActualHash: %s\n", qPrintable(actualHash)); 989 990 bool dumpImage = true; 991 992 if (!m_expectedHash.isEmpty()) { 993 Q_ASSERT(m_expectedHash.length() == 32); 994 fprintf(stdout, "\nExpectedHash: %s\n", qPrintable(m_expectedHash)); 995 996 if (m_expectedHash == actualHash) 997 dumpImage = false; 998 } 999 1000 if (dumpImage) { 1001 image.setText("checksum", actualHash); 1002 1003 QBuffer buffer; 1004 buffer.open(QBuffer::WriteOnly); 1005 image.save(&buffer, "PNG"); 1006 buffer.close(); 1007 const QByteArray &data = buffer.data(); 1008 1009 printf("Content-Type: %s\n", "image/png"); 1010 printf("Content-Length: %lu\n", static_cast<unsigned long>(data.length())); 1011 1012 const quint32 bytesToWriteInOneChunk = 1 << 15; 1013 quint32 dataRemainingToWrite = data.length(); 1014 const char *ptr = data.data(); 1015 while (dataRemainingToWrite) { 1016 quint32 bytesToWriteInThisChunk = qMin(dataRemainingToWrite, bytesToWriteInOneChunk); 1017 quint32 bytesWritten = fwrite(ptr, 1, bytesToWriteInThisChunk, stdout); 1018 if (bytesWritten != bytesToWriteInThisChunk) 1019 break; 1020 dataRemainingToWrite -= bytesWritten; 1021 ptr += bytesWritten; 1022 } 1023 } 1024 1025 fflush(stdout); 1026 } 1027 1028 puts("#EOF"); // terminate the (possibly empty) pixels block 1029 1030 fflush(stdout); 1031 fflush(stderr); 1032 1033 emit ready(); 1034} 1035 1036void DumpRenderTree::titleChanged(const QString &s) 1037{ 1038 if (m_controller->shouldDumpTitleChanges()) 1039 printf("TITLE CHANGED: %s\n", s.toUtf8().data()); 1040} 1041 1042void DumpRenderTree::connectFrame(QWebFrame *frame) 1043{ 1044 connect(frame, SIGNAL(javaScriptWindowObjectCleared()), this, SLOT(initJSObjects())); 1045 connect(frame, SIGNAL(provisionalLoad()), 1046 layoutTestController(), SLOT(provisionalLoad())); 1047} 1048 1049void DumpRenderTree::dumpDatabaseQuota(QWebFrame* frame, const QString& dbName) 1050{ 1051 if (!m_controller->shouldDumpDatabaseCallbacks()) 1052 return; 1053 QWebSecurityOrigin origin = frame->securityOrigin(); 1054 printf("UI DELEGATE DATABASE CALLBACK: exceededDatabaseQuotaForSecurityOrigin:{%s, %s, %i} database:%s\n", 1055 origin.scheme().toUtf8().data(), 1056 origin.host().toUtf8().data(), 1057 origin.port(), 1058 dbName.toUtf8().data()); 1059 origin.setDatabaseQuota(databaseDefaultQuota); 1060} 1061 1062void DumpRenderTree::dumpApplicationCacheQuota(QWebSecurityOrigin* origin, quint64 defaultOriginQuota) 1063{ 1064 if (!m_controller->shouldDumpApplicationCacheDelegateCallbacks()) 1065 return; 1066 1067 printf("UI DELEGATE APPLICATION CACHE CALLBACK: exceededApplicationCacheOriginQuotaForSecurityOrigin:{%s, %s, %i}\n", 1068 origin->scheme().toUtf8().data(), 1069 origin->host().toUtf8().data(), 1070 origin->port() 1071 ); 1072 origin->setApplicationCacheQuota(defaultOriginQuota); 1073} 1074 1075void DumpRenderTree::statusBarMessage(const QString& message) 1076{ 1077 if (!m_controller->shouldDumpStatusCallbacks()) 1078 return; 1079 1080 printf("UI DELEGATE STATUS CALLBACK: setStatusText:%s\n", message.toUtf8().constData()); 1081} 1082 1083QWebPage *DumpRenderTree::createWindow() 1084{ 1085 if (!m_controller->canOpenWindows()) 1086 return 0; 1087 1088 // Create a dummy container object to track the page in DRT. 1089 // QObject is used instead of QWidget to prevent DRT from 1090 // showing the main view when deleting the container. 1091 1092 QObject* container = new QObject(m_mainView); 1093 // create a QWebPage we want to return 1094 QWebPage* page = static_cast<QWebPage*>(new WebPage(container, this)); 1095 // gets cleaned up in closeRemainingWindows() 1096 windows.append(container); 1097 1098 // connect the needed signals to the page 1099 connect(page, SIGNAL(frameCreated(QWebFrame*)), this, SLOT(connectFrame(QWebFrame*))); 1100 connectFrame(page->mainFrame()); 1101 connect(page, SIGNAL(loadFinished(bool)), m_controller, SLOT(maybeDump(bool))); 1102 connect(page, SIGNAL(windowCloseRequested()), this, SLOT(windowCloseRequested())); 1103 1104 // Use a frame group name for all pages created by DumpRenderTree to allow 1105 // testing of cross-page frame lookup. 1106 DumpRenderTreeSupportQt::webPageSetGroupName(page, "org.webkit.qt.DumpRenderTree"); 1107 1108 return page; 1109} 1110 1111void DumpRenderTree::windowCloseRequested() 1112{ 1113 QWebPage* page = qobject_cast<QWebPage*>(sender()); 1114 QObject* container = page->parent(); 1115 windows.removeAll(container); 1116 container->deleteLater(); 1117} 1118 1119int DumpRenderTree::windowCount() const 1120{ 1121// include the main view in the count 1122 return windows.count() + 1; 1123} 1124 1125void DumpRenderTree::geolocationPermissionSet() 1126{ 1127 m_page->permissionSet(QWebPage::Geolocation); 1128} 1129 1130void DumpRenderTree::switchFocus(bool focused) 1131{ 1132 QFocusEvent event((focused) ? QEvent::FocusIn : QEvent::FocusOut, Qt::ActiveWindowFocusReason); 1133 if (!isGraphicsBased()) 1134 QApplication::sendEvent(m_mainView, &event); 1135 else { 1136 if (WebViewGraphicsBased* view = qobject_cast<WebViewGraphicsBased*>(m_mainView)) 1137 view->scene()->sendEvent(view->graphicsView(), &event); 1138 } 1139 1140} 1141 1142QList<WebPage*> DumpRenderTree::getAllPages() const 1143{ 1144 QList<WebPage*> pages; 1145 pages.append(m_page); 1146 foreach (QObject* widget, windows) { 1147 if (WebPage* page = widget->findChild<WebPage*>()) 1148 pages.append(page); 1149 } 1150 return pages; 1151} 1152 1153#if defined(Q_WS_X11) 1154void DumpRenderTree::initializeFonts() 1155{ 1156 static int numFonts = -1; 1157 1158 // Some test cases may add or remove application fonts (via @font-face). 1159 // Make sure to re-initialize the font set if necessary. 1160 FcFontSet* appFontSet = FcConfigGetFonts(0, FcSetApplication); 1161 if (appFontSet && numFonts >= 0 && appFontSet->nfont == numFonts) 1162 return; 1163 1164 QByteArray fontDir = getenv("WEBKIT_TESTFONTS"); 1165 if (fontDir.isEmpty() || !QDir(fontDir).exists()) { 1166 fprintf(stderr, 1167 "\n\n" 1168 "----------------------------------------------------------------------\n" 1169 "WEBKIT_TESTFONTS environment variable is not set correctly.\n" 1170 "This variable has to point to the directory containing the fonts\n" 1171 "you can clone from git://gitorious.org/qtwebkit/testfonts.git\n" 1172 "----------------------------------------------------------------------\n" 1173 ); 1174 exit(1); 1175 } 1176 char currentPath[PATH_MAX+1]; 1177 if (!getcwd(currentPath, PATH_MAX)) 1178 qFatal("Couldn't get current working directory"); 1179 QByteArray configFile = currentPath; 1180 FcConfig *config = FcConfigCreate(); 1181 configFile += "/Tools/DumpRenderTree/qt/fonts.conf"; 1182 if (!FcConfigParseAndLoad (config, (FcChar8*) configFile.data(), true)) 1183 qFatal("Couldn't load font configuration file"); 1184 if (!FcConfigAppFontAddDir (config, (FcChar8*) fontDir.data())) 1185 qFatal("Couldn't add font dir!"); 1186 FcConfigSetCurrent(config); 1187 1188 appFontSet = FcConfigGetFonts(config, FcSetApplication); 1189 numFonts = appFontSet->nfont; 1190} 1191#endif 1192 1193} 1194