| /* |
| * Copyright (C) 2005, 2006 Apple Computer, Inc. All rights reserved. |
| * Copyright (C) 2006 Nikolas Zimmermann <zimmermann@kde.org> |
| * Copyright (C) 2008 Nokia Corporation and/or its subsidiary(-ies) |
| * Copyright (C) 2009 Torch Mobile Inc. http://www.torchmobile.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 "DumpRenderTreeQt.h" |
| #include "DumpRenderTreeSupportQt.h" |
| #include "EventSenderQt.h" |
| #include "GCControllerQt.h" |
| #include "LayoutTestControllerQt.h" |
| #include "TextInputControllerQt.h" |
| #include "PlainTextControllerQt.h" |
| #include "testplugin.h" |
| #include "WorkQueue.h" |
| |
| #include <QApplication> |
| #include <QBuffer> |
| #include <QCryptographicHash> |
| #include <QDir> |
| #include <QFile> |
| #include <QFileInfo> |
| #include <QFocusEvent> |
| #include <QFontDatabase> |
| #include <QLocale> |
| #include <QNetworkAccessManager> |
| #include <QNetworkReply> |
| #include <QNetworkRequest> |
| #include <QPaintDevice> |
| #include <QPaintEngine> |
| #ifndef QT_NO_PRINTER |
| #include <QPrinter> |
| #endif |
| #include <QUndoStack> |
| #include <QUrl> |
| |
| #include <qwebsettings.h> |
| #include <qwebsecurityorigin.h> |
| |
| #ifndef QT_NO_UITOOLS |
| #include <QtUiTools/QUiLoader> |
| #endif |
| |
| #ifdef Q_WS_X11 |
| #include <fontconfig/fontconfig.h> |
| #endif |
| |
| #include <limits.h> |
| #include <locale.h> |
| |
| #ifndef Q_OS_WIN |
| #include <unistd.h> |
| #endif |
| |
| #include <qdebug.h> |
| |
| namespace WebCore { |
| |
| const int databaseDefaultQuota = 5 * 1024 * 1024; |
| |
| NetworkAccessManager::NetworkAccessManager(QObject* parent) |
| : QNetworkAccessManager(parent) |
| { |
| #ifndef QT_NO_OPENSSL |
| connect(this, SIGNAL(sslErrors(QNetworkReply*, const QList<QSslError>&)), |
| this, SLOT(sslErrorsEncountered(QNetworkReply*, const QList<QSslError>&))); |
| #endif |
| } |
| |
| #ifndef QT_NO_OPENSSL |
| void NetworkAccessManager::sslErrorsEncountered(QNetworkReply* reply, const QList<QSslError>& errors) |
| { |
| if (reply->url().host() == "127.0.0.1" || reply->url().host() == "localhost") { |
| bool ignore = true; |
| |
| // Accept any HTTPS certificate. |
| foreach (const QSslError& error, errors) { |
| if (error.error() < QSslError::UnableToGetIssuerCertificate || error.error() > QSslError::HostNameMismatch) { |
| ignore = false; |
| break; |
| } |
| } |
| |
| if (ignore) |
| reply->ignoreSslErrors(); |
| } |
| } |
| #endif |
| |
| |
| #ifndef QT_NO_PRINTER |
| class NullPrinter : public QPrinter { |
| public: |
| class NullPaintEngine : public QPaintEngine { |
| public: |
| virtual bool begin(QPaintDevice*) { return true; } |
| virtual bool end() { return true; } |
| virtual QPaintEngine::Type type() const { return QPaintEngine::User; } |
| virtual void drawPixmap(const QRectF& r, const QPixmap& pm, const QRectF& sr) { } |
| virtual void updateState(const QPaintEngineState& state) { } |
| }; |
| |
| virtual QPaintEngine* paintEngine() const { return const_cast<NullPaintEngine*>(&m_engine); } |
| |
| NullPaintEngine m_engine; |
| }; |
| #endif |
| |
| WebPage::WebPage(QObject* parent, DumpRenderTree* drt) |
| : QWebPage(parent) |
| , m_webInspector(0) |
| , m_drt(drt) |
| { |
| QWebSettings* globalSettings = QWebSettings::globalSettings(); |
| |
| globalSettings->setFontSize(QWebSettings::MinimumFontSize, 0); |
| globalSettings->setFontSize(QWebSettings::MinimumLogicalFontSize, 5); |
| globalSettings->setFontSize(QWebSettings::DefaultFontSize, 16); |
| globalSettings->setFontSize(QWebSettings::DefaultFixedFontSize, 13); |
| |
| globalSettings->setAttribute(QWebSettings::JavascriptCanOpenWindows, true); |
| globalSettings->setAttribute(QWebSettings::JavascriptCanAccessClipboard, true); |
| globalSettings->setAttribute(QWebSettings::LinksIncludedInFocusChain, false); |
| globalSettings->setAttribute(QWebSettings::PluginsEnabled, true); |
| globalSettings->setAttribute(QWebSettings::LocalContentCanAccessRemoteUrls, true); |
| globalSettings->setAttribute(QWebSettings::JavascriptEnabled, true); |
| globalSettings->setAttribute(QWebSettings::PrivateBrowsingEnabled, false); |
| globalSettings->setAttribute(QWebSettings::SpatialNavigationEnabled, false); |
| |
| connect(this, SIGNAL(geometryChangeRequested(const QRect &)), |
| this, SLOT(setViewGeometry(const QRect & ))); |
| |
| setNetworkAccessManager(m_drt->networkAccessManager()); |
| setPluginFactory(new TestPlugin(this)); |
| |
| connect(this, SIGNAL(featurePermissionRequested(QWebFrame*, QWebPage::Feature)), this, SLOT(requestPermission(QWebFrame*, QWebPage::Feature))); |
| connect(this, SIGNAL(featurePermissionRequestCanceled(QWebFrame*, QWebPage::Feature)), this, SLOT(cancelPermission(QWebFrame*, QWebPage::Feature))); |
| } |
| |
| WebPage::~WebPage() |
| { |
| delete m_webInspector; |
| } |
| |
| QWebInspector* WebPage::webInspector() |
| { |
| if (!m_webInspector) { |
| m_webInspector = new QWebInspector; |
| m_webInspector->setPage(this); |
| } |
| return m_webInspector; |
| } |
| |
| void WebPage::resetSettings() |
| { |
| // After each layout test, reset the settings that may have been changed by |
| // layoutTestController.overridePreference() or similar. |
| settings()->resetFontSize(QWebSettings::DefaultFontSize); |
| settings()->resetAttribute(QWebSettings::JavascriptCanOpenWindows); |
| settings()->resetAttribute(QWebSettings::JavascriptEnabled); |
| settings()->resetAttribute(QWebSettings::PrivateBrowsingEnabled); |
| settings()->resetAttribute(QWebSettings::SpatialNavigationEnabled); |
| settings()->resetAttribute(QWebSettings::LinksIncludedInFocusChain); |
| settings()->resetAttribute(QWebSettings::OfflineWebApplicationCacheEnabled); |
| settings()->resetAttribute(QWebSettings::LocalContentCanAccessRemoteUrls); |
| settings()->resetAttribute(QWebSettings::LocalContentCanAccessFileUrls); |
| settings()->resetAttribute(QWebSettings::PluginsEnabled); |
| settings()->resetAttribute(QWebSettings::JavascriptCanAccessClipboard); |
| settings()->resetAttribute(QWebSettings::AutoLoadImages); |
| |
| m_drt->layoutTestController()->setCaretBrowsingEnabled(false); |
| m_drt->layoutTestController()->setFrameFlatteningEnabled(false); |
| m_drt->layoutTestController()->setSmartInsertDeleteEnabled(true); |
| m_drt->layoutTestController()->setSelectTrailingWhitespaceEnabled(false); |
| |
| // globalSettings must be reset explicitly. |
| m_drt->layoutTestController()->setXSSAuditorEnabled(false); |
| |
| QWebSettings::setMaximumPagesInCache(0); // reset to default |
| settings()->setUserStyleSheetUrl(QUrl()); // reset to default |
| |
| DumpRenderTreeSupportQt::setMinimumTimerInterval(this, DumpRenderTreeSupportQt::defaultMinimumTimerInterval()); |
| |
| m_pendingGeolocationRequests.clear(); |
| } |
| |
| QWebPage *WebPage::createWindow(QWebPage::WebWindowType) |
| { |
| return m_drt->createWindow(); |
| } |
| |
| void WebPage::javaScriptAlert(QWebFrame*, const QString& message) |
| { |
| if (!isTextOutputEnabled()) |
| return; |
| |
| fprintf(stdout, "ALERT: %s\n", message.toUtf8().constData()); |
| } |
| |
| void WebPage::requestPermission(QWebFrame* frame, QWebPage::Feature feature) |
| { |
| switch (feature) { |
| case Notifications: |
| if (!m_drt->layoutTestController()->ignoreReqestForPermission()) |
| setFeaturePermission(frame, feature, PermissionGrantedByUser); |
| break; |
| case Geolocation: |
| if (m_drt->layoutTestController()->isGeolocationPermissionSet()) |
| if (m_drt->layoutTestController()->geolocationPermission()) |
| setFeaturePermission(frame, feature, PermissionGrantedByUser); |
| else |
| setFeaturePermission(frame, feature, PermissionDeniedByUser); |
| else |
| m_pendingGeolocationRequests.append(frame); |
| break; |
| default: |
| break; |
| } |
| } |
| |
| void WebPage::cancelPermission(QWebFrame* frame, QWebPage::Feature feature) |
| { |
| switch (feature) { |
| case Geolocation: |
| m_pendingGeolocationRequests.removeOne(frame); |
| break; |
| default: |
| break; |
| } |
| } |
| |
| void WebPage::permissionSet(QWebPage::Feature feature) |
| { |
| switch (feature) { |
| case Geolocation: |
| { |
| Q_ASSERT(m_drt->layoutTestController()->isGeolocationPermissionSet()); |
| foreach (QWebFrame* frame, m_pendingGeolocationRequests) |
| if (m_drt->layoutTestController()->geolocationPermission()) |
| setFeaturePermission(frame, feature, PermissionGrantedByUser); |
| else |
| setFeaturePermission(frame, feature, PermissionDeniedByUser); |
| |
| m_pendingGeolocationRequests.clear(); |
| break; |
| } |
| default: |
| break; |
| } |
| } |
| |
| static QString urlSuitableForTestResult(const QString& url) |
| { |
| if (url.isEmpty() || !url.startsWith(QLatin1String("file://"))) |
| return url; |
| |
| return QFileInfo(url).fileName(); |
| } |
| |
| void WebPage::javaScriptConsoleMessage(const QString& message, int lineNumber, const QString&) |
| { |
| if (!isTextOutputEnabled()) |
| return; |
| |
| QString newMessage; |
| if (!message.isEmpty()) { |
| newMessage = message; |
| |
| size_t fileProtocol = newMessage.indexOf(QLatin1String("file://")); |
| if (fileProtocol != -1) { |
| newMessage = newMessage.left(fileProtocol) + urlSuitableForTestResult(newMessage.mid(fileProtocol)); |
| } |
| } |
| |
| fprintf (stdout, "CONSOLE MESSAGE: line %d: %s\n", lineNumber, newMessage.toUtf8().constData()); |
| } |
| |
| bool WebPage::javaScriptConfirm(QWebFrame*, const QString& msg) |
| { |
| if (!isTextOutputEnabled()) |
| return true; |
| |
| fprintf(stdout, "CONFIRM: %s\n", msg.toUtf8().constData()); |
| return true; |
| } |
| |
| bool WebPage::javaScriptPrompt(QWebFrame*, const QString& msg, const QString& defaultValue, QString* result) |
| { |
| if (!isTextOutputEnabled()) |
| return true; |
| |
| fprintf(stdout, "PROMPT: %s, default text: %s\n", msg.toUtf8().constData(), defaultValue.toUtf8().constData()); |
| *result = defaultValue; |
| return true; |
| } |
| |
| bool WebPage::acceptNavigationRequest(QWebFrame* frame, const QNetworkRequest& request, NavigationType type) |
| { |
| if (m_drt->layoutTestController()->waitForPolicy()) { |
| QString url = QString::fromUtf8(request.url().toEncoded()); |
| QString typeDescription; |
| |
| switch (type) { |
| case NavigationTypeLinkClicked: |
| typeDescription = "link clicked"; |
| break; |
| case NavigationTypeFormSubmitted: |
| typeDescription = "form submitted"; |
| break; |
| case NavigationTypeBackOrForward: |
| typeDescription = "back/forward"; |
| break; |
| case NavigationTypeReload: |
| typeDescription = "reload"; |
| break; |
| case NavigationTypeFormResubmitted: |
| typeDescription = "form resubmitted"; |
| break; |
| case NavigationTypeOther: |
| typeDescription = "other"; |
| break; |
| default: |
| typeDescription = "illegal value"; |
| } |
| |
| if (isTextOutputEnabled()) |
| fprintf(stdout, "Policy delegate: attempt to load %s with navigation type '%s'\n", |
| url.toUtf8().constData(), typeDescription.toUtf8().constData()); |
| |
| m_drt->layoutTestController()->notifyDone(); |
| } |
| return QWebPage::acceptNavigationRequest(frame, request, type); |
| } |
| |
| bool WebPage::supportsExtension(QWebPage::Extension extension) const |
| { |
| if (extension == QWebPage::ErrorPageExtension) |
| return m_drt->layoutTestController()->shouldHandleErrorPages(); |
| |
| return false; |
| } |
| |
| bool WebPage::extension(Extension extension, const ExtensionOption *option, ExtensionReturn *output) |
| { |
| const QWebPage::ErrorPageExtensionOption* info = static_cast<const QWebPage::ErrorPageExtensionOption*>(option); |
| |
| // Lets handle error pages for the main frame for now. |
| if (info->frame != mainFrame()) |
| return false; |
| |
| QWebPage::ErrorPageExtensionReturn* errorPage = static_cast<QWebPage::ErrorPageExtensionReturn*>(output); |
| |
| errorPage->content = QString("data:text/html,<body/>").toUtf8(); |
| |
| return true; |
| } |
| |
| QObject* WebPage::createPlugin(const QString& classId, const QUrl& url, const QStringList& paramNames, const QStringList& paramValues) |
| { |
| Q_UNUSED(url); |
| Q_UNUSED(paramNames); |
| Q_UNUSED(paramValues); |
| #ifndef QT_NO_UITOOLS |
| QUiLoader loader; |
| return loader.createWidget(classId, view()); |
| #else |
| Q_UNUSED(classId); |
| return 0; |
| #endif |
| } |
| |
| void WebPage::setViewGeometry(const QRect& rect) |
| { |
| if (WebViewGraphicsBased* v = qobject_cast<WebViewGraphicsBased*>(view())) |
| v->scene()->setSceneRect(QRectF(rect)); |
| else if (QWidget *v = view()) |
| v->setGeometry(rect); |
| } |
| |
| WebViewGraphicsBased::WebViewGraphicsBased(QWidget* parent) |
| : m_item(new QGraphicsWebView) |
| { |
| setScene(new QGraphicsScene(this)); |
| scene()->addItem(m_item); |
| } |
| |
| DumpRenderTree::DumpRenderTree() |
| : m_dumpPixels(false) |
| , m_stdin(0) |
| , m_enableTextOutput(false) |
| , m_standAloneMode(false) |
| , m_graphicsBased(false) |
| , m_persistentStoragePath(QString(getenv("DUMPRENDERTREE_TEMP"))) |
| { |
| QByteArray viewMode = getenv("QT_DRT_WEBVIEW_MODE"); |
| if (viewMode == "graphics") |
| setGraphicsBased(true); |
| |
| // Set running in DRT mode for qwebpage to create testable objects. |
| DumpRenderTreeSupportQt::setDumpRenderTreeModeEnabled(true); |
| DumpRenderTreeSupportQt::overwritePluginDirectories(); |
| DumpRenderTreeSupportQt::activeMockDeviceOrientationClient(true); |
| QWebSettings::enablePersistentStorage(m_persistentStoragePath); |
| |
| m_networkAccessManager = new NetworkAccessManager(this); |
| // create our primary testing page/view. |
| if (isGraphicsBased()) { |
| WebViewGraphicsBased* view = new WebViewGraphicsBased(0); |
| m_page = new WebPage(view, this); |
| view->setPage(m_page); |
| m_mainView = view; |
| } else { |
| QWebView* view = new QWebView(0); |
| m_page = new WebPage(view, this); |
| view->setPage(m_page); |
| m_mainView = view; |
| } |
| // Use a frame group name for all pages created by DumpRenderTree to allow |
| // testing of cross-page frame lookup. |
| DumpRenderTreeSupportQt::webPageSetGroupName(m_page, "org.webkit.qt.DumpRenderTree"); |
| |
| m_mainView->setContextMenuPolicy(Qt::NoContextMenu); |
| m_mainView->resize(QSize(LayoutTestController::maxViewWidth, LayoutTestController::maxViewHeight)); |
| |
| // clean up cache by resetting quota. |
| qint64 quota = webPage()->settings()->offlineWebApplicationCacheQuota(); |
| webPage()->settings()->setOfflineWebApplicationCacheQuota(quota); |
| |
| // create our controllers. This has to be done before connectFrame, |
| // as it exports there to the JavaScript DOM window. |
| m_controller = new LayoutTestController(this); |
| connect(m_controller, SIGNAL(showPage()), this, SLOT(showPage())); |
| connect(m_controller, SIGNAL(hidePage()), this, SLOT(hidePage())); |
| |
| // async geolocation permission set by controller |
| connect(m_controller, SIGNAL(geolocationPermissionSet()), this, SLOT(geolocationPermissionSet())); |
| |
| connect(m_controller, SIGNAL(done()), this, SLOT(dump())); |
| m_eventSender = new EventSender(m_page); |
| m_textInputController = new TextInputController(m_page); |
| m_plainTextController = new PlainTextController(m_page); |
| m_gcController = new GCController(m_page); |
| |
| // now connect our different signals |
| connect(m_page, SIGNAL(frameCreated(QWebFrame *)), |
| this, SLOT(connectFrame(QWebFrame *))); |
| connectFrame(m_page->mainFrame()); |
| |
| connect(m_page, SIGNAL(loadFinished(bool)), |
| m_controller, SLOT(maybeDump(bool))); |
| // We need to connect to loadStarted() because notifyDone should only |
| // dump results itself when the last page loaded in the test has finished loading. |
| connect(m_page, SIGNAL(loadStarted()), |
| m_controller, SLOT(resetLoadFinished())); |
| connect(m_page, SIGNAL(windowCloseRequested()), this, SLOT(windowCloseRequested())); |
| connect(m_page, SIGNAL(printRequested(QWebFrame*)), this, SLOT(dryRunPrint(QWebFrame*))); |
| |
| connect(m_page->mainFrame(), SIGNAL(titleChanged(const QString&)), |
| SLOT(titleChanged(const QString&))); |
| connect(m_page, SIGNAL(databaseQuotaExceeded(QWebFrame*,QString)), |
| this, SLOT(dumpDatabaseQuota(QWebFrame*,QString))); |
| connect(m_page, SIGNAL(applicationCacheQuotaExceeded(QWebSecurityOrigin *, quint64)), |
| this, SLOT(dumpApplicationCacheQuota(QWebSecurityOrigin *, quint64))); |
| connect(m_page, SIGNAL(statusBarMessage(const QString&)), |
| this, SLOT(statusBarMessage(const QString&))); |
| |
| QObject::connect(this, SIGNAL(quit()), qApp, SLOT(quit()), Qt::QueuedConnection); |
| |
| DumpRenderTreeSupportQt::setDumpRenderTreeModeEnabled(true); |
| QFocusEvent event(QEvent::FocusIn, Qt::ActiveWindowFocusReason); |
| QApplication::sendEvent(m_mainView, &event); |
| } |
| |
| DumpRenderTree::~DumpRenderTree() |
| { |
| if (!m_redirectOutputFileName.isEmpty()) |
| fclose(stdout); |
| if (!m_redirectErrorFileName.isEmpty()) |
| fclose(stderr); |
| delete m_mainView; |
| delete m_stdin; |
| DumpRenderTreeSupportQt::removeMockDeviceOrientation(); |
| } |
| |
| static void clearHistory(QWebPage* page) |
| { |
| // QWebHistory::clear() leaves current page, so remove it as well by setting |
| // max item count to 0, and then setting it back to it's original value. |
| |
| QWebHistory* history = page->history(); |
| int itemCount = history->maximumItemCount(); |
| |
| history->clear(); |
| history->setMaximumItemCount(0); |
| history->setMaximumItemCount(itemCount); |
| } |
| |
| void DumpRenderTree::dryRunPrint(QWebFrame* frame) |
| { |
| #ifndef QT_NO_PRINTER |
| NullPrinter printer; |
| frame->print(&printer); |
| #endif |
| } |
| |
| void DumpRenderTree::resetToConsistentStateBeforeTesting(const QUrl& url) |
| { |
| // reset so that any current loads are stopped |
| // NOTE: that this has to be done before the layoutTestController is |
| // reset or we get timeouts for some tests. |
| m_page->blockSignals(true); |
| m_page->triggerAction(QWebPage::Stop); |
| m_page->blockSignals(false); |
| |
| QList<QWebSecurityOrigin> knownOrigins = QWebSecurityOrigin::allOrigins(); |
| for (int i = 0; i < knownOrigins.size(); ++i) |
| knownOrigins[i].setDatabaseQuota(databaseDefaultQuota); |
| |
| // reset the layoutTestController at this point, so that we under no |
| // circumstance dump (stop the waitUntilDone timer) during the reset |
| // of the DRT. |
| m_controller->reset(); |
| |
| // reset mouse clicks counter |
| m_eventSender->resetClickCount(); |
| |
| closeRemainingWindows(); |
| |
| m_page->resetSettings(); |
| #ifndef QT_NO_UNDOSTACK |
| m_page->undoStack()->clear(); |
| #endif |
| m_page->mainFrame()->setZoomFactor(1.0); |
| clearHistory(m_page); |
| DumpRenderTreeSupportQt::clearFrameName(m_page->mainFrame()); |
| |
| m_page->mainFrame()->setScrollBarPolicy(Qt::Vertical, Qt::ScrollBarAsNeeded); |
| m_page->mainFrame()->setScrollBarPolicy(Qt::Horizontal, Qt::ScrollBarAsNeeded); |
| |
| if (url.scheme() == "http" || url.scheme() == "https") { |
| // credentials may exist from previous tests. |
| m_page->setNetworkAccessManager(0); |
| delete m_networkAccessManager; |
| m_networkAccessManager = new NetworkAccessManager(this); |
| m_page->setNetworkAccessManager(m_networkAccessManager); |
| } |
| |
| WorkQueue::shared()->clear(); |
| WorkQueue::shared()->setFrozen(false); |
| |
| DumpRenderTreeSupportQt::resetOriginAccessWhiteLists(); |
| |
| // Qt defaults to Windows editing behavior. |
| DumpRenderTreeSupportQt::setEditingBehavior(m_page, "win"); |
| |
| QLocale::setDefault(QLocale::c()); |
| |
| layoutTestController()->setDeveloperExtrasEnabled(true); |
| #ifndef Q_OS_WINCE |
| setlocale(LC_ALL, ""); |
| #endif |
| |
| DumpRenderTreeSupportQt::clearOpener(m_page->mainFrame()); |
| } |
| |
| static bool isGlobalHistoryTest(const QUrl& url) |
| { |
| if (url.path().contains("globalhistory/")) |
| return true; |
| return false; |
| } |
| |
| static bool isWebInspectorTest(const QUrl& url) |
| { |
| if (url.path().contains("inspector/")) |
| return true; |
| return false; |
| } |
| |
| static bool isDumpAsTextTest(const QUrl& url) |
| { |
| if (url.path().contains("dumpAsText/")) |
| return true; |
| return false; |
| } |
| |
| |
| void DumpRenderTree::open(const QUrl& url) |
| { |
| DumpRenderTreeSupportQt::dumpResourceLoadCallbacksPath(QFileInfo(url.toString()).path()); |
| resetToConsistentStateBeforeTesting(url); |
| |
| if (isWebInspectorTest(m_page->mainFrame()->url())) |
| layoutTestController()->closeWebInspector(); |
| |
| if (isWebInspectorTest(url)) |
| layoutTestController()->showWebInspector(); |
| |
| if (isDumpAsTextTest(url)) { |
| layoutTestController()->dumpAsText(); |
| setDumpPixels(false); |
| } |
| |
| if (isGlobalHistoryTest(url)) |
| layoutTestController()->dumpHistoryCallbacks(); |
| |
| // W3C SVG tests expect to be 480x360 |
| bool isW3CTest = url.toString().contains("svg/W3C-SVG-1.1"); |
| int width = isW3CTest ? 480 : LayoutTestController::maxViewWidth; |
| int height = isW3CTest ? 360 : LayoutTestController::maxViewHeight; |
| m_mainView->resize(QSize(width, height)); |
| m_page->setPreferredContentsSize(QSize()); |
| m_page->setViewportSize(QSize(width, height)); |
| |
| QFocusEvent ev(QEvent::FocusIn); |
| m_page->event(&ev); |
| |
| QWebSettings::clearMemoryCaches(); |
| #if !(defined(Q_OS_SYMBIAN) && QT_VERSION <= QT_VERSION_CHECK(4, 6, 2)) |
| QFontDatabase::removeAllApplicationFonts(); |
| #endif |
| #if defined(Q_WS_X11) |
| initializeFonts(); |
| #endif |
| |
| DumpRenderTreeSupportQt::dumpFrameLoader(url.toString().contains("loading/")); |
| setTextOutputEnabled(true); |
| m_page->mainFrame()->load(url); |
| } |
| |
| void DumpRenderTree::readLine() |
| { |
| if (!m_stdin) { |
| m_stdin = new QFile; |
| m_stdin->open(stdin, QFile::ReadOnly); |
| |
| if (!m_stdin->isReadable()) { |
| emit quit(); |
| return; |
| } |
| } |
| |
| QByteArray line = m_stdin->readLine().trimmed(); |
| |
| if (line.isEmpty()) { |
| emit quit(); |
| return; |
| } |
| |
| processLine(QString::fromLocal8Bit(line.constData(), line.length())); |
| } |
| |
| void DumpRenderTree::processArgsLine(const QStringList &args) |
| { |
| setStandAloneMode(true); |
| |
| m_standAloneModeTestList = args; |
| |
| QFileInfo firstEntry(m_standAloneModeTestList.first()); |
| if (firstEntry.isDir()) { |
| QDir folderEntry(m_standAloneModeTestList.first()); |
| QStringList supportedExt; |
| // Check for all supported extensions (from Scripts/webkitpy/layout_tests/layout_package/test_files.py). |
| supportedExt << "*.html" << "*.shtml" << "*.xml" << "*.xhtml" << "*.xhtmlmp" << "*.pl" << "*.php" << "*.svg"; |
| m_standAloneModeTestList = folderEntry.entryList(supportedExt, QDir::Files); |
| for (int i = 0; i < m_standAloneModeTestList.size(); ++i) |
| m_standAloneModeTestList[i] = folderEntry.absoluteFilePath(m_standAloneModeTestList[i]); |
| } |
| connect(this, SIGNAL(ready()), this, SLOT(loadNextTestInStandAloneMode())); |
| |
| if (!m_standAloneModeTestList.isEmpty()) { |
| QString first = m_standAloneModeTestList.takeFirst(); |
| processLine(first); |
| } |
| } |
| |
| void DumpRenderTree::loadNextTestInStandAloneMode() |
| { |
| if (m_standAloneModeTestList.isEmpty()) { |
| emit quit(); |
| return; |
| } |
| QString first = m_standAloneModeTestList.takeFirst(); |
| processLine(first); |
| } |
| |
| void DumpRenderTree::processLine(const QString &input) |
| { |
| QString line = input; |
| |
| m_expectedHash = QString(); |
| if (m_dumpPixels) { |
| // single quote marks the pixel dump hash |
| int i = line.indexOf('\''); |
| if (i > -1) { |
| m_expectedHash = line.mid(i + 1, line.length()); |
| line.remove(i, line.length()); |
| } |
| } |
| |
| if (line.startsWith(QLatin1String("http:")) |
| || line.startsWith(QLatin1String("https:")) |
| || line.startsWith(QLatin1String("file:"))) { |
| open(QUrl(line)); |
| } else { |
| QFileInfo fi(line); |
| |
| if (!fi.exists()) { |
| QDir currentDir = QDir::currentPath(); |
| |
| // Try to be smart about where the test is located |
| if (currentDir.dirName() == QLatin1String("LayoutTests")) |
| fi = QFileInfo(currentDir, line.replace(QRegExp(".*?LayoutTests/(.*)"), "\\1")); |
| else if (!line.contains(QLatin1String("LayoutTests"))) |
| fi = QFileInfo(currentDir, line.prepend(QLatin1String("LayoutTests/"))); |
| |
| if (!fi.exists()) { |
| emit ready(); |
| return; |
| } |
| } |
| |
| open(QUrl::fromLocalFile(fi.absoluteFilePath())); |
| } |
| |
| fflush(stdout); |
| } |
| |
| void DumpRenderTree::setDumpPixels(bool dump) |
| { |
| m_dumpPixels = dump; |
| } |
| |
| void DumpRenderTree::closeRemainingWindows() |
| { |
| foreach (QObject* widget, windows) |
| delete widget; |
| windows.clear(); |
| } |
| |
| void DumpRenderTree::initJSObjects() |
| { |
| QWebFrame *frame = qobject_cast<QWebFrame*>(sender()); |
| Q_ASSERT(frame); |
| frame->addToJavaScriptWindowObject(QLatin1String("layoutTestController"), m_controller); |
| frame->addToJavaScriptWindowObject(QLatin1String("eventSender"), m_eventSender); |
| frame->addToJavaScriptWindowObject(QLatin1String("textInputController"), m_textInputController); |
| frame->addToJavaScriptWindowObject(QLatin1String("GCController"), m_gcController); |
| frame->addToJavaScriptWindowObject(QLatin1String("plainText"), m_plainTextController); |
| } |
| |
| void DumpRenderTree::showPage() |
| { |
| m_mainView->show(); |
| // we need a paint event but cannot process all the events |
| QPixmap pixmap(m_mainView->size()); |
| m_mainView->render(&pixmap); |
| } |
| |
| void DumpRenderTree::hidePage() |
| { |
| m_mainView->hide(); |
| } |
| |
| QString DumpRenderTree::dumpFrameScrollPosition(QWebFrame* frame) |
| { |
| if (!frame || !DumpRenderTreeSupportQt::hasDocumentElement(frame)) |
| return QString(); |
| |
| QString result; |
| QPoint pos = frame->scrollPosition(); |
| if (pos.x() > 0 || pos.y() > 0) { |
| QWebFrame* parent = qobject_cast<QWebFrame *>(frame->parent()); |
| if (parent) |
| result.append(QString("frame '%1' ").arg(frame->title())); |
| result.append(QString("scrolled to %1,%2\n").arg(pos.x()).arg(pos.y())); |
| } |
| |
| if (m_controller->shouldDumpChildFrameScrollPositions()) { |
| QList<QWebFrame*> children = frame->childFrames(); |
| for (int i = 0; i < children.size(); ++i) |
| result += dumpFrameScrollPosition(children.at(i)); |
| } |
| return result; |
| } |
| |
| QString DumpRenderTree::dumpFramesAsText(QWebFrame* frame) |
| { |
| if (!frame || !DumpRenderTreeSupportQt::hasDocumentElement(frame)) |
| return QString(); |
| |
| QString result; |
| QWebFrame* parent = qobject_cast<QWebFrame*>(frame->parent()); |
| if (parent) { |
| result.append(QLatin1String("\n--------\nFrame: '")); |
| result.append(frame->frameName()); |
| result.append(QLatin1String("'\n--------\n")); |
| } |
| |
| QString innerText = frame->toPlainText(); |
| result.append(innerText); |
| result.append(QLatin1String("\n")); |
| |
| if (m_controller->shouldDumpChildrenAsText()) { |
| QList<QWebFrame *> children = frame->childFrames(); |
| for (int i = 0; i < children.size(); ++i) |
| result += dumpFramesAsText(children.at(i)); |
| } |
| |
| return result; |
| } |
| |
| static QString dumpHistoryItem(const QWebHistoryItem& item, int indent, bool current) |
| { |
| QString result; |
| |
| int start = 0; |
| if (current) { |
| result.append(QLatin1String("curr->")); |
| start = 6; |
| } |
| for (int i = start; i < indent; i++) |
| result.append(' '); |
| |
| QString url = item.url().toEncoded(); |
| if (url.contains("file://")) { |
| static QString layoutTestsString("/LayoutTests/"); |
| static QString fileTestString("(file test):"); |
| |
| QString res = url.mid(url.indexOf(layoutTestsString) + layoutTestsString.length()); |
| if (res.isEmpty()) |
| return result; |
| |
| result.append(fileTestString); |
| result.append(res); |
| } else { |
| result.append(url); |
| } |
| |
| QString target = DumpRenderTreeSupportQt::historyItemTarget(item); |
| if (!target.isEmpty()) |
| result.append(QString(QLatin1String(" (in frame \"%1\")")).arg(target)); |
| |
| if (DumpRenderTreeSupportQt::isTargetItem(item)) |
| result.append(QLatin1String(" **nav target**")); |
| result.append(QLatin1String("\n")); |
| |
| QMap<QString, QWebHistoryItem> children = DumpRenderTreeSupportQt::getChildHistoryItems(item); |
| foreach (QWebHistoryItem item, children) |
| result += dumpHistoryItem(item, 12, false); |
| |
| return result; |
| } |
| |
| QString DumpRenderTree::dumpBackForwardList(QWebPage* page) |
| { |
| QWebHistory* history = page->history(); |
| |
| QString result; |
| result.append(QLatin1String("\n============== Back Forward List ==============\n")); |
| |
| // FORMAT: |
| // " (file test):fast/loader/resources/click-fragment-link.html **nav target**" |
| // "curr-> (file test):fast/loader/resources/click-fragment-link.html#testfragment **nav target**" |
| |
| int maxItems = history->maximumItemCount(); |
| |
| foreach (const QWebHistoryItem item, history->backItems(maxItems)) { |
| if (!item.isValid()) |
| continue; |
| result.append(dumpHistoryItem(item, 8, false)); |
| } |
| |
| QWebHistoryItem item = history->currentItem(); |
| if (item.isValid()) |
| result.append(dumpHistoryItem(item, 8, true)); |
| |
| foreach (const QWebHistoryItem item, history->forwardItems(maxItems)) { |
| if (!item.isValid()) |
| continue; |
| result.append(dumpHistoryItem(item, 8, false)); |
| } |
| |
| result.append(QLatin1String("===============================================\n")); |
| return result; |
| } |
| |
| static const char *methodNameStringForFailedTest(LayoutTestController *controller) |
| { |
| const char *errorMessage; |
| if (controller->shouldDumpAsText()) |
| errorMessage = "[documentElement innerText]"; |
| // FIXME: Add when we have support |
| //else if (controller->dumpDOMAsWebArchive()) |
| // errorMessage = "[[mainFrame DOMDocument] webArchive]"; |
| //else if (controller->dumpSourceAsWebArchive()) |
| // errorMessage = "[[mainFrame dataSource] webArchive]"; |
| else |
| errorMessage = "[mainFrame renderTreeAsExternalRepresentation]"; |
| |
| return errorMessage; |
| } |
| |
| void DumpRenderTree::dump() |
| { |
| // Prevent any further frame load or resource load callbacks from appearing after we dump the result. |
| DumpRenderTreeSupportQt::dumpFrameLoader(false); |
| DumpRenderTreeSupportQt::dumpResourceLoadCallbacks(false); |
| |
| QWebFrame *mainFrame = m_page->mainFrame(); |
| |
| if (isStandAloneMode()) { |
| QString markup = mainFrame->toHtml(); |
| fprintf(stdout, "Source:\n\n%s\n", markup.toUtf8().constData()); |
| } |
| |
| QString mimeType = DumpRenderTreeSupportQt::responseMimeType(mainFrame); |
| if (mimeType == "text/plain") |
| m_controller->dumpAsText(); |
| |
| // Dump render text... |
| QString resultString; |
| if (m_controller->shouldDumpAsText()) |
| resultString = dumpFramesAsText(mainFrame); |
| else { |
| resultString = mainFrame->renderTreeDump(); |
| resultString += dumpFrameScrollPosition(mainFrame); |
| } |
| if (!resultString.isEmpty()) { |
| fprintf(stdout, "Content-Type: text/plain\n"); |
| fprintf(stdout, "%s", resultString.toUtf8().constData()); |
| |
| if (m_controller->shouldDumpBackForwardList()) { |
| fprintf(stdout, "%s", dumpBackForwardList(webPage()).toUtf8().constData()); |
| foreach (QObject* widget, windows) { |
| QWebPage* page = qobject_cast<QWebPage*>(widget->findChild<QWebPage*>()); |
| fprintf(stdout, "%s", dumpBackForwardList(page).toUtf8().constData()); |
| } |
| } |
| |
| } else |
| printf("ERROR: nil result from %s", methodNameStringForFailedTest(m_controller)); |
| |
| // signal end of text block |
| fputs("#EOF\n", stdout); |
| fputs("#EOF\n", stderr); |
| |
| // FIXME: All other ports don't dump pixels, if generatePixelResults is false. |
| if (m_dumpPixels) { |
| QImage image(m_page->viewportSize(), QImage::Format_ARGB32); |
| image.fill(Qt::white); |
| QPainter painter(&image); |
| mainFrame->render(&painter); |
| painter.end(); |
| |
| QCryptographicHash hash(QCryptographicHash::Md5); |
| for (int row = 0; row < image.height(); ++row) |
| hash.addData(reinterpret_cast<const char*>(image.scanLine(row)), image.width() * 4); |
| QString actualHash = hash.result().toHex(); |
| |
| fprintf(stdout, "\nActualHash: %s\n", qPrintable(actualHash)); |
| |
| bool dumpImage = true; |
| |
| if (!m_expectedHash.isEmpty()) { |
| Q_ASSERT(m_expectedHash.length() == 32); |
| fprintf(stdout, "\nExpectedHash: %s\n", qPrintable(m_expectedHash)); |
| |
| if (m_expectedHash == actualHash) |
| dumpImage = false; |
| } |
| |
| if (dumpImage) { |
| image.setText("checksum", actualHash); |
| |
| QBuffer buffer; |
| buffer.open(QBuffer::WriteOnly); |
| image.save(&buffer, "PNG"); |
| buffer.close(); |
| const QByteArray &data = buffer.data(); |
| |
| printf("Content-Type: %s\n", "image/png"); |
| printf("Content-Length: %lu\n", static_cast<unsigned long>(data.length())); |
| |
| const quint32 bytesToWriteInOneChunk = 1 << 15; |
| quint32 dataRemainingToWrite = data.length(); |
| const char *ptr = data.data(); |
| while (dataRemainingToWrite) { |
| quint32 bytesToWriteInThisChunk = qMin(dataRemainingToWrite, bytesToWriteInOneChunk); |
| quint32 bytesWritten = fwrite(ptr, 1, bytesToWriteInThisChunk, stdout); |
| if (bytesWritten != bytesToWriteInThisChunk) |
| break; |
| dataRemainingToWrite -= bytesWritten; |
| ptr += bytesWritten; |
| } |
| } |
| |
| fflush(stdout); |
| } |
| |
| puts("#EOF"); // terminate the (possibly empty) pixels block |
| |
| fflush(stdout); |
| fflush(stderr); |
| |
| emit ready(); |
| } |
| |
| void DumpRenderTree::titleChanged(const QString &s) |
| { |
| if (m_controller->shouldDumpTitleChanges()) |
| printf("TITLE CHANGED: %s\n", s.toUtf8().data()); |
| } |
| |
| void DumpRenderTree::connectFrame(QWebFrame *frame) |
| { |
| connect(frame, SIGNAL(javaScriptWindowObjectCleared()), this, SLOT(initJSObjects())); |
| connect(frame, SIGNAL(provisionalLoad()), |
| layoutTestController(), SLOT(provisionalLoad())); |
| } |
| |
| void DumpRenderTree::dumpDatabaseQuota(QWebFrame* frame, const QString& dbName) |
| { |
| if (!m_controller->shouldDumpDatabaseCallbacks()) |
| return; |
| QWebSecurityOrigin origin = frame->securityOrigin(); |
| printf("UI DELEGATE DATABASE CALLBACK: exceededDatabaseQuotaForSecurityOrigin:{%s, %s, %i} database:%s\n", |
| origin.scheme().toUtf8().data(), |
| origin.host().toUtf8().data(), |
| origin.port(), |
| dbName.toUtf8().data()); |
| origin.setDatabaseQuota(databaseDefaultQuota); |
| } |
| |
| void DumpRenderTree::dumpApplicationCacheQuota(QWebSecurityOrigin* origin, quint64 defaultOriginQuota) |
| { |
| if (!m_controller->shouldDumpApplicationCacheDelegateCallbacks()) |
| return; |
| |
| printf("UI DELEGATE APPLICATION CACHE CALLBACK: exceededApplicationCacheOriginQuotaForSecurityOrigin:{%s, %s, %i}\n", |
| origin->scheme().toUtf8().data(), |
| origin->host().toUtf8().data(), |
| origin->port() |
| ); |
| origin->setApplicationCacheQuota(defaultOriginQuota); |
| } |
| |
| void DumpRenderTree::statusBarMessage(const QString& message) |
| { |
| if (!m_controller->shouldDumpStatusCallbacks()) |
| return; |
| |
| printf("UI DELEGATE STATUS CALLBACK: setStatusText:%s\n", message.toUtf8().constData()); |
| } |
| |
| QWebPage *DumpRenderTree::createWindow() |
| { |
| if (!m_controller->canOpenWindows()) |
| return 0; |
| |
| // Create a dummy container object to track the page in DRT. |
| // QObject is used instead of QWidget to prevent DRT from |
| // showing the main view when deleting the container. |
| |
| QObject* container = new QObject(m_mainView); |
| // create a QWebPage we want to return |
| QWebPage* page = static_cast<QWebPage*>(new WebPage(container, this)); |
| // gets cleaned up in closeRemainingWindows() |
| windows.append(container); |
| |
| // connect the needed signals to the page |
| connect(page, SIGNAL(frameCreated(QWebFrame*)), this, SLOT(connectFrame(QWebFrame*))); |
| connectFrame(page->mainFrame()); |
| connect(page, SIGNAL(loadFinished(bool)), m_controller, SLOT(maybeDump(bool))); |
| connect(page, SIGNAL(windowCloseRequested()), this, SLOT(windowCloseRequested())); |
| |
| // Use a frame group name for all pages created by DumpRenderTree to allow |
| // testing of cross-page frame lookup. |
| DumpRenderTreeSupportQt::webPageSetGroupName(page, "org.webkit.qt.DumpRenderTree"); |
| |
| return page; |
| } |
| |
| void DumpRenderTree::windowCloseRequested() |
| { |
| QWebPage* page = qobject_cast<QWebPage*>(sender()); |
| QObject* container = page->parent(); |
| windows.removeAll(container); |
| container->deleteLater(); |
| } |
| |
| int DumpRenderTree::windowCount() const |
| { |
| // include the main view in the count |
| return windows.count() + 1; |
| } |
| |
| void DumpRenderTree::geolocationPermissionSet() |
| { |
| m_page->permissionSet(QWebPage::Geolocation); |
| } |
| |
| void DumpRenderTree::switchFocus(bool focused) |
| { |
| QFocusEvent event((focused) ? QEvent::FocusIn : QEvent::FocusOut, Qt::ActiveWindowFocusReason); |
| if (!isGraphicsBased()) |
| QApplication::sendEvent(m_mainView, &event); |
| else { |
| if (WebViewGraphicsBased* view = qobject_cast<WebViewGraphicsBased*>(m_mainView)) |
| view->scene()->sendEvent(view->graphicsView(), &event); |
| } |
| |
| } |
| |
| QList<WebPage*> DumpRenderTree::getAllPages() const |
| { |
| QList<WebPage*> pages; |
| pages.append(m_page); |
| foreach (QObject* widget, windows) { |
| if (WebPage* page = widget->findChild<WebPage*>()) |
| pages.append(page); |
| } |
| return pages; |
| } |
| |
| #if defined(Q_WS_X11) |
| void DumpRenderTree::initializeFonts() |
| { |
| static int numFonts = -1; |
| |
| // Some test cases may add or remove application fonts (via @font-face). |
| // Make sure to re-initialize the font set if necessary. |
| FcFontSet* appFontSet = FcConfigGetFonts(0, FcSetApplication); |
| if (appFontSet && numFonts >= 0 && appFontSet->nfont == numFonts) |
| return; |
| |
| QByteArray fontDir = getenv("WEBKIT_TESTFONTS"); |
| if (fontDir.isEmpty() || !QDir(fontDir).exists()) { |
| fprintf(stderr, |
| "\n\n" |
| "----------------------------------------------------------------------\n" |
| "WEBKIT_TESTFONTS environment variable is not set correctly.\n" |
| "This variable has to point to the directory containing the fonts\n" |
| "you can clone from git://gitorious.org/qtwebkit/testfonts.git\n" |
| "----------------------------------------------------------------------\n" |
| ); |
| exit(1); |
| } |
| char currentPath[PATH_MAX+1]; |
| if (!getcwd(currentPath, PATH_MAX)) |
| qFatal("Couldn't get current working directory"); |
| QByteArray configFile = currentPath; |
| FcConfig *config = FcConfigCreate(); |
| configFile += "/Tools/DumpRenderTree/qt/fonts.conf"; |
| if (!FcConfigParseAndLoad (config, (FcChar8*) configFile.data(), true)) |
| qFatal("Couldn't load font configuration file"); |
| if (!FcConfigAppFontAddDir (config, (FcChar8*) fontDir.data())) |
| qFatal("Couldn't add font dir!"); |
| FcConfigSetCurrent(config); |
| |
| appFontSet = FcConfigGetFonts(config, FcSetApplication); |
| numFonts = appFontSet->nfont; |
| } |
| #endif |
| |
| } |