diff --git a/tests/auto/shared/httpreqrep.cpp b/tests/auto/shared/httpreqrep.cpp index eb2db68901ac4bb72cf2c9f45440c893c44b8a35..15a86631cd846a534932fc740b553cc738b73fd0 100644 --- a/tests/auto/shared/httpreqrep.cpp +++ b/tests/auto/shared/httpreqrep.cpp @@ -31,11 +31,14 @@ HttpReqRep::HttpReqRep(QTcpSocket *socket, QObject *parent) : QObject(parent), m_socket(socket) { m_socket->setParent(this); - connect(m_socket, &QIODevice::readyRead, this, &HttpReqRep::handleReadyRead); + connect(m_socket, &QTcpSocket::readyRead, this, &HttpReqRep::handleReadyRead); + connect(m_socket, &QTcpSocket::disconnected, this, &HttpReqRep::handleDisconnected); } void HttpReqRep::sendResponse() { + if (m_state != State::REQUEST_RECEIVED) + return; m_socket->write("HTTP/1.1 "); m_socket->write(QByteArray::number(m_responseStatusCode)); m_socket->write(" OK?\r\n"); @@ -45,9 +48,21 @@ void HttpReqRep::sendResponse() m_socket->write(kv.second); m_socket->write("\r\n"); } + m_socket->write("Connection: close\r\n"); m_socket->write("\r\n"); m_socket->write(m_responseBody); - m_socket->close(); + m_state = State::DISCONNECTING; + m_socket->disconnectFromHost(); + Q_EMIT responseSent(); +} + +void HttpReqRep::close() +{ + if (m_state != State::REQUEST_RECEIVED) + return; + m_state = State::DISCONNECTING; + m_socket->disconnectFromHost(); + Q_EMIT error(QStringLiteral("missing response")); } QByteArray HttpReqRep::requestHeader(const QByteArray &key) const @@ -60,32 +75,60 @@ QByteArray HttpReqRep::requestHeader(const QByteArray &key) const void HttpReqRep::handleReadyRead() { - const auto requestLine = m_socket->readLine(); - const auto requestLineParts = requestLine.split(' '); - if (requestLineParts.size() != 3 || !requestLineParts[2].toUpper().startsWith("HTTP/")) { - qWarning("HttpReqRep: invalid request line"); - Q_EMIT readFinished(false); - return; - } - - decltype(m_requestHeaders) headers; - for (;;) { - const auto headerLine = m_socket->readLine(); - if (headerLine == QByteArrayLiteral("\r\n")) + while (m_socket->canReadLine()) { + switch (m_state) { + case State::RECEIVING_REQUEST: { + const auto requestLine = m_socket->readLine(); + const auto requestLineParts = requestLine.split(' '); + if (requestLineParts.size() != 3 || !requestLineParts[2].toUpper().startsWith("HTTP/")) { + m_state = State::DISCONNECTING; + m_socket->disconnectFromHost(); + Q_EMIT error(QStringLiteral("invalid request line")); + return; + } + m_requestMethod = requestLineParts[0]; + m_requestPath = requestLineParts[1]; + m_state = State::RECEIVING_HEADERS; + break; + } + case State::RECEIVING_HEADERS: { + const auto headerLine = m_socket->readLine(); + if (headerLine == QByteArrayLiteral("\r\n")) { + m_state = State::REQUEST_RECEIVED; + Q_EMIT requestReceived(); + return; + } + int colonIndex = headerLine.indexOf(':'); + if (colonIndex < 0) { + m_state = State::DISCONNECTING; + m_socket->disconnectFromHost(); + Q_EMIT error(QStringLiteral("invalid header line")); + return; + } + auto headerKey = headerLine.left(colonIndex).trimmed().toLower(); + auto headerValue = headerLine.mid(colonIndex + 1).trimmed().toLower(); + m_requestHeaders.emplace(headerKey, headerValue); break; - int colonIndex = headerLine.indexOf(':'); - if (colonIndex < 0) { - qWarning("HttpReqRep: invalid header line"); - Q_EMIT readFinished(false); + } + default: return; } - auto headerKey = headerLine.left(colonIndex).trimmed().toLower(); - auto headerValue = headerLine.mid(colonIndex + 1).trimmed().toLower(); - headers.emplace(headerKey, headerValue); } +} - m_requestMethod = requestLineParts[0]; - m_requestPath = requestLineParts[1]; - m_requestHeaders = headers; - Q_EMIT readFinished(true); +void HttpReqRep::handleDisconnected() +{ + switch (m_state) { + case State::RECEIVING_REQUEST: + case State::RECEIVING_HEADERS: + case State::REQUEST_RECEIVED: + Q_EMIT error(QStringLiteral("unexpected disconnect")); + break; + case State::DISCONNECTING: + break; + case State::DISCONNECTED: + Q_UNREACHABLE(); + } + m_state = State::DISCONNECTED; + Q_EMIT closed(); } diff --git a/tests/auto/shared/httpreqrep.h b/tests/auto/shared/httpreqrep.h index 4e9f10dff3c5b56b3d1e2c9f3ba87c54938d35dd..bee8119eb7dbe9d6f3276d6e406b393bce2a6a23 100644 --- a/tests/auto/shared/httpreqrep.h +++ b/tests/auto/shared/httpreqrep.h @@ -30,6 +30,7 @@ #include <QTcpSocket> +#include <map> #include <utility> // Represents an HTTP request-response exchange. @@ -37,11 +38,20 @@ class HttpReqRep : public QObject { Q_OBJECT public: - HttpReqRep(QTcpSocket *socket, QObject *parent = nullptr); + explicit HttpReqRep(QTcpSocket *socket, QObject *parent = nullptr); + void sendResponse(); + void close(); + + // Request parameters (only valid after requestReceived()) + QByteArray requestMethod() const { return m_requestMethod; } QByteArray requestPath() const { return m_requestPath; } QByteArray requestHeader(const QByteArray &key) const; + + // Response parameters (can be set until sendResponse()/close()). + + int responseStatus() const { return m_responseStatusCode; } void setResponseStatus(int statusCode) { m_responseStatusCode = statusCode; @@ -50,6 +60,7 @@ public: { m_responseHeaders[key.toLower()] = std::move(value); } + QByteArray responseBody() const { return m_responseBody; } void setResponseBody(QByteArray content) { m_responseHeaders["content-length"] = QByteArray::number(content.size()); @@ -57,13 +68,34 @@ public: } Q_SIGNALS: - void readFinished(bool ok); + // Emitted when the request has been correctly parsed. + void requestReceived(); + // Emitted on first call to sendResponse(). + void responseSent(); + // Emitted when something goes wrong. + void error(const QString &error); + // Emitted during or some time after sendResponse() or close(). + void closed(); private Q_SLOTS: void handleReadyRead(); + void handleDisconnected(); private: + enum class State { + // Waiting for first line of request. + RECEIVING_REQUEST, // Next: RECEIVING_HEADERS or DISCONNECTING. + // Waiting for header lines. + RECEIVING_HEADERS, // Next: REQUEST_RECEIVED or DISCONNECTING. + // Request parsing succeeded, waiting for sendResponse() or close(). + REQUEST_RECEIVED, // Next: DISCONNECTING. + // Waiting for network. + DISCONNECTING, // Next: DISCONNECTED. + // Connection is dead. + DISCONNECTED, // Next: - + }; QTcpSocket *m_socket = nullptr; + State m_state = State::RECEIVING_REQUEST; QByteArray m_requestMethod; QByteArray m_requestPath; std::map<QByteArray, QByteArray> m_requestHeaders; diff --git a/tests/auto/shared/httpserver.cpp b/tests/auto/shared/httpserver.cpp index 6012379f2a68d63039f4808656d42db551166842..8d14c18ff6fcad409d2585a55b4735e9c8f858a1 100644 --- a/tests/auto/shared/httpserver.cpp +++ b/tests/auto/shared/httpserver.cpp @@ -27,44 +27,59 @@ ****************************************************************************/ #include "httpserver.h" -#include "waitforsignal.h" +#include <QLoggingCategory> + +Q_LOGGING_CATEGORY(gHttpServerLog, "HttpServer") HttpServer::HttpServer(QObject *parent) : QObject(parent) { connect(&m_tcpServer, &QTcpServer::newConnection, this, &HttpServer::handleNewConnection); - if (!m_tcpServer.listen()) - qWarning("HttpServer: listen() failed"); - m_url = QStringLiteral("http://127.0.0.1:") + QString::number(m_tcpServer.serverPort()); } -QUrl HttpServer::url(const QString &path) const +bool HttpServer::start() { - auto copy = m_url; - copy.setPath(path); - return copy; + m_error = false; + + if (!m_tcpServer.listen()) { + qCWarning(gHttpServerLog).noquote() << m_tcpServer.errorString(); + return false; + } + + m_url.setScheme(QStringLiteral("http")); + m_url.setHost(QStringLiteral("127.0.0.1")); + m_url.setPort(m_tcpServer.serverPort()); + + return true; } -void HttpServer::handleNewConnection() +bool HttpServer::stop() { - auto reqRep = new HttpReqRep(m_tcpServer.nextPendingConnection(), this); - connect(reqRep, &HttpReqRep::readFinished, this, &HttpServer::handleReadFinished); + m_tcpServer.close(); + return !m_error; } -void HttpServer::handleReadFinished(bool ok) +QUrl HttpServer::url(const QString &path) const { - auto reqRep = qobject_cast<HttpReqRep *>(sender()); - if (ok) - Q_EMIT newRequest(reqRep); - else - reqRep->deleteLater(); + auto copy = m_url; + copy.setPath(path); + return copy; } -std::unique_ptr<HttpReqRep> waitForRequest(HttpServer *server) +void HttpServer::handleNewConnection() { - std::unique_ptr<HttpReqRep> result; - waitForSignal(server, &HttpServer::newRequest, [&](HttpReqRep *rr) { - rr->setParent(nullptr); - result.reset(rr); + auto rr = new HttpReqRep(m_tcpServer.nextPendingConnection(), this); + connect(rr, &HttpReqRep::requestReceived, [this, rr]() { + Q_EMIT newRequest(rr); + rr->close(); + }); + connect(rr, &HttpReqRep::responseSent, [this, rr]() { + qCInfo(gHttpServerLog).noquote() << rr->requestMethod() << rr->requestPath() + << rr->responseStatus() << rr->responseBody().size(); + }); + connect(rr, &HttpReqRep::error, [this, rr](const QString &error) { + qCWarning(gHttpServerLog).noquote() << rr->requestMethod() << rr->requestPath() + << error; + m_error = true; }); - return result; + connect(rr, &HttpReqRep::closed, rr, &QObject::deleteLater); } diff --git a/tests/auto/shared/httpserver.h b/tests/auto/shared/httpserver.h index e45743b7b0e19aba95e7eca43d2f176182a746d9..ddbab433ccd0db45a5896f928617b2a78d693462 100644 --- a/tests/auto/shared/httpserver.h +++ b/tests/auto/shared/httpserver.h @@ -33,29 +33,55 @@ #include <QTcpServer> #include <QUrl> -#include <memory> - // Listens on a TCP socket and creates HttpReqReps for each connection. +// +// Usage: +// +// HttpServer server; +// connect(&server, &HttpServer::newRequest, [](HttpReqRep *rr) { +// if (rr->requestPath() == "/myPage.html") { +// rr->setResponseBody("<html><body>Hello, World!</body></html>"); +// rr->sendResponse(); +// } +// }); +// QVERIFY(server.start()); +// /* do stuff */ +// QVERIFY(server.stop()); +// +// HttpServer owns the HttpReqRep objects. The signal handler should not store +// references to HttpReqRep objects. +// +// Only if a handler calls sendResponse() will a response be actually sent. This +// means that multiple handlers can be connected to the signal, with different +// handlers responsible for different paths. class HttpServer : public QObject { Q_OBJECT +public: + explicit HttpServer(QObject *parent = nullptr); - QTcpServer m_tcpServer; - QUrl m_url; + // Must be called to start listening. + // + // Returns true if a TCP port has been successfully bound. + Q_REQUIRED_RESULT bool start(); -public: - HttpServer(QObject *parent = nullptr); + // Stops listening and performs final error checks. + Q_REQUIRED_RESULT bool stop(); + + // Full URL for given relative path QUrl url(const QString &path = QStringLiteral("/")) const; Q_SIGNALS: + // Emitted after a HTTP request has been successfully parsed. void newRequest(HttpReqRep *reqRep); private Q_SLOTS: void handleNewConnection(); - void handleReadFinished(bool ok); -}; -// Wait for an HTTP request to be received. -std::unique_ptr<HttpReqRep> waitForRequest(HttpServer *server); +private: + QTcpServer m_tcpServer; + QUrl m_url; + bool m_error = false; +}; #endif // !HTTPSERVER_H diff --git a/tests/auto/shared/waitforsignal.h b/tests/auto/shared/waitforsignal.h deleted file mode 100644 index 9b2daf7af03c5033df955227517de5773927c5f7..0000000000000000000000000000000000000000 --- a/tests/auto/shared/waitforsignal.h +++ /dev/null @@ -1,90 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2017 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the QtWebEngine module of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:GPL-EXCEPT$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 3 as published by the Free Software -** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ -#ifndef WAITFORSIGNAL_H -#define WAITFORSIGNAL_H - -#include <QObject> -#include <QTestEventLoop> - -#include <utility> - -// Implementation details of waitForSignal. -namespace { - // Wraps a functor to set a flag and exit from event loop if called. - template <class SignalHandler> - struct waitForSignal_SignalHandlerWrapper { - waitForSignal_SignalHandlerWrapper(SignalHandler &&sh) - : signalHandler(std::forward<SignalHandler>(sh)) {} - - template <class... Args> - void operator()(Args && ... args) { - signalHandler(std::forward<Args>(args)...); - hasBeenCalled = true; - loop.exitLoop(); - } - - SignalHandler &&signalHandler; - QTestEventLoop loop; - bool hasBeenCalled = false; - }; - - // No-op functor. - struct waitForSignal_DefaultSignalHandler { - template <class... Args> - void operator()(Args && ...) {} - }; -} // namespace - -// Wait for a signal to be emitted. -// -// The given functor is called with the signal arguments allowing the arguments -// to be modified before returning from the signal handler (unlike QSignalSpy). -template <class Sender, class Signal, class SignalHandler> -bool waitForSignal(Sender &&sender, Signal &&signal, SignalHandler &&signalHandler, int timeout = 15000) -{ - waitForSignal_SignalHandlerWrapper<SignalHandler> signalHandlerWrapper( - std::forward<SignalHandler>(signalHandler)); - auto connection = QObject::connect( - std::forward<Sender>(sender), - std::forward<Signal>(signal), - std::ref(signalHandlerWrapper)); - signalHandlerWrapper.loop.enterLoopMSecs(timeout); - QObject::disconnect(connection); - return signalHandlerWrapper.hasBeenCalled; -} - -template <class Sender, class Signal> -bool waitForSignal(Sender &&sender, Signal &&signal, int timeout = 15000) -{ - return waitForSignal(std::forward<Sender>(sender), - std::forward<Signal>(signal), - waitForSignal_DefaultSignalHandler(), - timeout); -} - -#endif // !WAITFORSIGNAL_H diff --git a/tests/auto/widgets/qwebenginedownloads/BLACKLIST b/tests/auto/widgets/qwebenginedownloads/BLACKLIST deleted file mode 100644 index 1bf8c8b1f9d483c517a63faca9cabb7bad9e9288..0000000000000000000000000000000000000000 --- a/tests/auto/widgets/qwebenginedownloads/BLACKLIST +++ /dev/null @@ -1,4 +0,0 @@ -[downloadLink] -osx -[downloadTwoLinks] -* diff --git a/tests/auto/widgets/qwebenginedownloads/tst_qwebenginedownloads.cpp b/tests/auto/widgets/qwebenginedownloads/tst_qwebenginedownloads.cpp index e8ac9676fc1aef0842d8f4f3911c96c00534d673..4848038df58644251f857928e9408c043f9fc991 100644 --- a/tests/auto/widgets/qwebenginedownloads/tst_qwebenginedownloads.cpp +++ b/tests/auto/widgets/qwebenginedownloads/tst_qwebenginedownloads.cpp @@ -36,50 +36,150 @@ #include <QWebEngineProfile> #include <QWebEngineView> #include <httpserver.h> -#include <waitforsignal.h> - -static std::unique_ptr<HttpReqRep> waitForFaviconRequest(HttpServer *server) -{ - auto rr = waitForRequest(server); - if (!rr || - rr->requestMethod() != QByteArrayLiteral("GET") || - rr->requestPath() != QByteArrayLiteral("/favicon.ico")) - return nullptr; - rr->setResponseStatus(404); - rr->sendResponse(); - return std::move(rr); -} class tst_QWebEngineDownloads : public QObject { Q_OBJECT + +public: + enum UserAction { + SaveLink, + ClickLink, + }; + + enum FileAction { + FileIsDownloaded, + FileIsDisplayed, + }; + private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + void cleanupTestCase(); + void downloadLink_data(); void downloadLink(); + void downloadTwoLinks_data(); void downloadTwoLinks(); void downloadPage_data(); void downloadPage(); void downloadViaSetUrl(); void downloadFileNot1(); void downloadFileNot2(); -}; -enum DownloadTestUserAction { - SaveLink, - Navigate, +private: + void saveLink(QPoint linkPos); + void clickLink(QPoint linkPos); + void simulateUserAction(QPoint linkPos, UserAction action); + + QWebEngineDownloadItem::DownloadType expectedDownloadType( + UserAction userAction, + const QByteArray &contentDisposition = QByteArray()); + + HttpServer *m_server; + QWebEngineProfile *m_profile; + QWebEnginePage *m_page; + QWebEngineView *m_view; + QSet<QWebEngineDownloadItem *> m_downloads; }; -enum DownloadTestFileAction { - FileIsDownloaded, - FileIsDisplayed, +class ScopedConnection { +public: + ScopedConnection(QMetaObject::Connection connection) : m_connection(std::move(connection)) {} + ~ScopedConnection() { QObject::disconnect(m_connection); } +private: + QMetaObject::Connection m_connection; }; -Q_DECLARE_METATYPE(DownloadTestUserAction); -Q_DECLARE_METATYPE(DownloadTestFileAction); +Q_DECLARE_METATYPE(tst_QWebEngineDownloads::UserAction) +Q_DECLARE_METATYPE(tst_QWebEngineDownloads::FileAction) + +void tst_QWebEngineDownloads::initTestCase() +{ + m_server = new HttpServer(); + m_profile = new QWebEngineProfile; + m_profile->setHttpCacheType(QWebEngineProfile::NoCache); + connect(m_profile, &QWebEngineProfile::downloadRequested, [this](QWebEngineDownloadItem *item) { + m_downloads.insert(item); + connect(item, &QWebEngineDownloadItem::destroyed, [this, item](){ + m_downloads.remove(item); + }); + connect(item, &QWebEngineDownloadItem::finished, [this, item](){ + m_downloads.remove(item); + }); + }); + m_page = new QWebEnginePage(m_profile); + m_view = new QWebEngineView; + m_view->setPage(m_page); + m_view->show(); +} + +void tst_QWebEngineDownloads::init() +{ + QVERIFY(m_server->start()); +} + +void tst_QWebEngineDownloads::cleanup() +{ + QCOMPARE(m_downloads.count(), 0); + QVERIFY(m_server->stop()); +} + +void tst_QWebEngineDownloads::cleanupTestCase() +{ + delete m_view; + delete m_page; + delete m_profile; + delete m_server; +} + +void tst_QWebEngineDownloads::saveLink(QPoint linkPos) +{ + // Simulate right-clicking on link and choosing "save link as" from menu. + QSignalSpy menuSpy(m_view, &QWebEngineView::customContextMenuRequested); + m_view->setContextMenuPolicy(Qt::CustomContextMenu); + auto event1 = new QContextMenuEvent(QContextMenuEvent::Mouse, linkPos); + auto event2 = new QMouseEvent(QEvent::MouseButtonPress, linkPos, Qt::RightButton, {}, {}); + auto event3 = new QMouseEvent(QEvent::MouseButtonRelease, linkPos, Qt::RightButton, {}, {}); + QTRY_VERIFY(m_view->focusWidget()); + QWidget *renderWidget = m_view->focusWidget(); + QCoreApplication::postEvent(renderWidget, event1); + QCoreApplication::postEvent(renderWidget, event2); + QCoreApplication::postEvent(renderWidget, event3); + QTRY_COMPARE(menuSpy.count(), 1); + m_page->triggerAction(QWebEnginePage::DownloadLinkToDisk); +} + +void tst_QWebEngineDownloads::clickLink(QPoint linkPos) +{ + // Simulate left-clicking on link. + QTRY_VERIFY(m_view->focusWidget()); + QWidget *renderWidget = m_view->focusWidget(); + QTest::mouseClick(renderWidget, Qt::LeftButton, {}, linkPos); +} + +void tst_QWebEngineDownloads::simulateUserAction(QPoint linkPos, UserAction action) +{ + switch (action) { + case SaveLink: return saveLink(linkPos); + case ClickLink: return clickLink(linkPos); + } +} + +QWebEngineDownloadItem::DownloadType tst_QWebEngineDownloads::expectedDownloadType( + UserAction userAction, const QByteArray &contentDisposition) +{ + if (userAction == SaveLink) + return QWebEngineDownloadItem::UserRequested; + if (contentDisposition == QByteArrayLiteral("attachment")) + return QWebEngineDownloadItem::Attachment; + return QWebEngineDownloadItem::DownloadAttribute; +} void tst_QWebEngineDownloads::downloadLink_data() { - QTest::addColumn<DownloadTestUserAction>("userAction"); + QTest::addColumn<UserAction>("userAction"); QTest::addColumn<bool>("anchorHasDownloadAttribute"); QTest::addColumn<QByteArray>("fileName"); QTest::addColumn<QByteArray>("fileContents"); @@ -87,7 +187,7 @@ void tst_QWebEngineDownloads::downloadLink_data() QTest::addColumn<QByteArray>("fileMimeTypeDetected"); QTest::addColumn<QByteArray>("fileDisposition"); QTest::addColumn<bool>("fileHasReferer"); - QTest::addColumn<DownloadTestFileAction>("fileAction"); + QTest::addColumn<FileAction>("fileAction"); QTest::addColumn<QWebEngineDownloadItem::DownloadType>("downloadType"); // SaveLink should always trigger a download, even for empty files. @@ -100,8 +200,7 @@ void tst_QWebEngineDownloads::downloadLink_data() /* fileMimeTypeDetected */ << QByteArrayLiteral("") /* fileDisposition */ << QByteArrayLiteral("") /* fileHasReferer */ << true - /* fileAction */ << FileIsDownloaded - /* downloadType */ << QWebEngineDownloadItem::UserRequested; + /* fileAction */ << FileIsDownloaded; // SaveLink should always trigger a download, also for text files. QTest::newRow("save link to text file") @@ -113,8 +212,7 @@ void tst_QWebEngineDownloads::downloadLink_data() /* fileMimeTypeDetected */ << QByteArrayLiteral("text/plain") /* fileDisposition */ << QByteArrayLiteral("") /* fileHasReferer */ << true - /* fileAction */ << FileIsDownloaded - /* downloadType */ << QWebEngineDownloadItem::UserRequested; + /* fileAction */ << FileIsDownloaded; // ... adding the "download" attribute should have no effect. QTest::newRow("save link to text file (attribute)") @@ -126,8 +224,7 @@ void tst_QWebEngineDownloads::downloadLink_data() /* fileMimeTypeDetected */ << QByteArrayLiteral("text/plain") /* fileDisposition */ << QByteArrayLiteral("") /* fileHasReferer */ << true - /* fileAction */ << FileIsDownloaded - /* downloadType */ << QWebEngineDownloadItem::UserRequested; + /* fileAction */ << FileIsDownloaded; // ... adding the "attachment" content disposition should also have no effect. QTest::newRow("save link to text file (attachment)") @@ -139,8 +236,7 @@ void tst_QWebEngineDownloads::downloadLink_data() /* fileMimeTypeDetected */ << QByteArrayLiteral("text/plain") /* fileDisposition */ << QByteArrayLiteral("attachment") /* fileHasReferer */ << true - /* fileAction */ << FileIsDownloaded - /* downloadType */ << QWebEngineDownloadItem::UserRequested; + /* fileAction */ << FileIsDownloaded; // ... even adding both should have no effect. QTest::newRow("save link to text file (attribute+attachment)") @@ -152,12 +248,11 @@ void tst_QWebEngineDownloads::downloadLink_data() /* fileMimeTypeDetected */ << QByteArrayLiteral("text/plain") /* fileDisposition */ << QByteArrayLiteral("attachment") /* fileHasReferer */ << true - /* fileAction */ << FileIsDownloaded - /* downloadType */ << QWebEngineDownloadItem::UserRequested; + /* fileAction */ << FileIsDownloaded; // Navigating to an empty file should show an empty page. QTest::newRow("navigate to empty file") - /* userAction */ << Navigate + /* userAction */ << ClickLink /* anchorHasDownloadAttribute */ << false /* fileName */ << QByteArrayLiteral("foo.txt") /* fileContents */ << QByteArrayLiteral("") @@ -165,12 +260,11 @@ void tst_QWebEngineDownloads::downloadLink_data() /* fileMimeTypeDetected */ << QByteArrayLiteral("") /* fileDisposition */ << QByteArrayLiteral("") /* fileHasReferer */ << true - /* fileAction */ << FileIsDisplayed - /* downloadType */ << QWebEngineDownloadItem::UserRequested; + /* fileAction */ << FileIsDisplayed; // Navigating to a text file should show the text file. QTest::newRow("navigate to text file") - /* userAction */ << Navigate + /* userAction */ << ClickLink /* anchorHasDownloadAttribute */ << false /* fileName */ << QByteArrayLiteral("foo.txt") /* fileContents */ << QByteArrayLiteral("bar") @@ -178,12 +272,11 @@ void tst_QWebEngineDownloads::downloadLink_data() /* fileMimeTypeDetected */ << QByteArrayLiteral("text/plain") /* fileDisposition */ << QByteArrayLiteral("") /* fileHasReferer */ << true - /* fileAction */ << FileIsDisplayed - /* downloadType */ << QWebEngineDownloadItem::UserRequested; + /* fileAction */ << FileIsDisplayed; // ... unless the link has the "download" attribute: then the file should be downloaded. QTest::newRow("navigate to text file (attribute)") - /* userAction */ << Navigate + /* userAction */ << ClickLink /* anchorHasDownloadAttribute */ << true /* fileName */ << QByteArrayLiteral("foo.txt") /* fileContents */ << QByteArrayLiteral("bar") @@ -191,12 +284,11 @@ void tst_QWebEngineDownloads::downloadLink_data() /* fileMimeTypeDetected */ << QByteArrayLiteral("text/plain") /* fileDisposition */ << QByteArrayLiteral("") /* fileHasReferer */ << true - /* fileAction */ << FileIsDownloaded - /* downloadType */ << QWebEngineDownloadItem::DownloadAttribute; + /* fileAction */ << FileIsDownloaded; // ... same with the content disposition header save for the download type. QTest::newRow("navigate to text file (attachment)") - /* userAction */ << Navigate + /* userAction */ << ClickLink /* anchorHasDownloadAttribute */ << false /* fileName */ << QByteArrayLiteral("foo.txt") /* fileContents */ << QByteArrayLiteral("bar") @@ -204,12 +296,11 @@ void tst_QWebEngineDownloads::downloadLink_data() /* fileMimeTypeDetected */ << QByteArrayLiteral("text/plain") /* fileDisposition */ << QByteArrayLiteral("attachment") /* fileHasReferer */ << true - /* fileAction */ << FileIsDownloaded - /* downloadType */ << QWebEngineDownloadItem::Attachment; + /* fileAction */ << FileIsDownloaded; // ... and both. QTest::newRow("navigate to text file (attribute+attachment)") - /* userAction */ << Navigate + /* userAction */ << ClickLink /* anchorHasDownloadAttribute */ << true /* fileName */ << QByteArrayLiteral("foo.txt") /* fileContents */ << QByteArrayLiteral("bar") @@ -217,12 +308,11 @@ void tst_QWebEngineDownloads::downloadLink_data() /* fileMimeTypeDetected */ << QByteArrayLiteral("text/plain") /* fileDisposition */ << QByteArrayLiteral("attachment") /* fileHasReferer */ << true - /* fileAction */ << FileIsDownloaded - /* downloadType */ << QWebEngineDownloadItem::Attachment; + /* fileAction */ << FileIsDownloaded; // The file's extension has no effect. QTest::newRow("navigate to supposed zip file") - /* userAction */ << Navigate + /* userAction */ << ClickLink /* anchorHasDownloadAttribute */ << false /* fileName */ << QByteArrayLiteral("foo.zip") /* fileContents */ << QByteArrayLiteral("bar") @@ -230,12 +320,11 @@ void tst_QWebEngineDownloads::downloadLink_data() /* fileMimeTypeDetected */ << QByteArrayLiteral("") /* fileDisposition */ << QByteArrayLiteral("") /* fileHasReferer */ << true - /* fileAction */ << FileIsDisplayed - /* downloadType */ << QWebEngineDownloadItem::DownloadAttribute; + /* fileAction */ << FileIsDisplayed; // ... the file's mime type however does. QTest::newRow("navigate to supposed zip file (application/zip)") - /* userAction */ << Navigate + /* userAction */ << ClickLink /* anchorHasDownloadAttribute */ << false /* fileName */ << QByteArrayLiteral("foo.zip") /* fileContents */ << QByteArrayLiteral("bar") @@ -243,12 +332,11 @@ void tst_QWebEngineDownloads::downloadLink_data() /* fileMimeTypeDetected */ << QByteArrayLiteral("application/zip") /* fileDisposition */ << QByteArrayLiteral("") /* fileHasReferer */ << true - /* fileAction */ << FileIsDownloaded - /* downloadType */ << QWebEngineDownloadItem::DownloadAttribute; + /* fileAction */ << FileIsDownloaded; // ... but we're not very picky about the exact type. QTest::newRow("navigate to supposed zip file (application/octet-stream)") - /* userAction */ << Navigate + /* userAction */ << ClickLink /* anchorHasDownloadAttribute */ << false /* fileName */ << QByteArrayLiteral("foo.zip") /* fileContents */ << QByteArrayLiteral("bar") @@ -256,15 +344,14 @@ void tst_QWebEngineDownloads::downloadLink_data() /* fileMimeTypeDetected */ << QByteArrayLiteral("application/octet-stream") /* fileDisposition */ << QByteArrayLiteral("") /* fileHasReferer */ << true - /* fileAction */ << FileIsDownloaded - /* downloadType */ << QWebEngineDownloadItem::DownloadAttribute; + /* fileAction */ << FileIsDownloaded; // empty zip file (consisting only of "end of central directory record") QByteArray zipFile = QByteArrayLiteral("PK\x05\x06") + QByteArray(18, 0); // The mime type is guessed automatically if not provided by the server. QTest::newRow("navigate to actual zip file") - /* userAction */ << Navigate + /* userAction */ << ClickLink /* anchorHasDownloadAttribute */ << false /* fileName */ << QByteArrayLiteral("foo.zip") /* fileContents */ << zipFile @@ -272,12 +359,11 @@ void tst_QWebEngineDownloads::downloadLink_data() /* fileMimeTypeDetected */ << QByteArrayLiteral("application/octet-stream") /* fileDisposition */ << QByteArrayLiteral("") /* fileHasReferer */ << true - /* fileAction */ << FileIsDownloaded - /* downloadType */ << QWebEngineDownloadItem::DownloadAttribute; + /* fileAction */ << FileIsDownloaded; // The mime type is not guessed automatically if provided by the server. QTest::newRow("navigate to actual zip file (application/zip)") - /* userAction */ << Navigate + /* userAction */ << ClickLink /* anchorHasDownloadAttribute */ << false /* fileName */ << QByteArrayLiteral("foo.zip") /* fileContents */ << zipFile @@ -285,13 +371,12 @@ void tst_QWebEngineDownloads::downloadLink_data() /* fileMimeTypeDetected */ << QByteArrayLiteral("application/zip") /* fileDisposition */ << QByteArrayLiteral("") /* fileHasReferer */ << true - /* fileAction */ << FileIsDownloaded - /* downloadType */ << QWebEngineDownloadItem::DownloadAttribute; + /* fileAction */ << FileIsDownloaded; } void tst_QWebEngineDownloads::downloadLink() { - QFETCH(DownloadTestUserAction, userAction); + QFETCH(UserAction, userAction); QFETCH(bool, anchorHasDownloadAttribute); QFETCH(QByteArray, fileName); QFETCH(QByteArray, fileContents); @@ -299,245 +384,225 @@ void tst_QWebEngineDownloads::downloadLink() QFETCH(QByteArray, fileMimeTypeDetected); QFETCH(QByteArray, fileDisposition); QFETCH(bool, fileHasReferer); - QFETCH(DownloadTestFileAction, fileAction); - QFETCH(QWebEngineDownloadItem::DownloadType, downloadType); - - HttpServer server; - QWebEngineProfile profile; - profile.setHttpCacheType(QWebEngineProfile::NoCache); - QWebEnginePage page(&profile); - QWebEngineView view; - view.setPage(&page); - - // 1. Load an HTML page with a link - // - // The only variation being whether the <a> element has a "download" - // attribute or not. - view.load(server.url()); - view.show(); - auto indexRR = waitForRequest(&server); - QVERIFY(indexRR); - QCOMPARE(indexRR->requestMethod(), QByteArrayLiteral("GET")); - QCOMPARE(indexRR->requestPath(), QByteArrayLiteral("/")); - indexRR->setResponseHeader(QByteArrayLiteral("content-type"), QByteArrayLiteral("text/html")); - QByteArray html; - html += QByteArrayLiteral("<html><body><a href=\""); - html += fileName; - html += QByteArrayLiteral("\" "); - if (anchorHasDownloadAttribute) - html += QByteArrayLiteral("download"); - html += QByteArrayLiteral(">Link</a></body></html>"); - indexRR->setResponseBody(html); - indexRR->sendResponse(); - bool loadOk = false; - QVERIFY(waitForSignal(&page, &QWebEnginePage::loadFinished, [&](bool ok) { loadOk = ok; })); - QVERIFY(loadOk); - - // 1.1. Ignore favicon request - auto favIconRR = waitForFaviconRequest(&server); - QVERIFY(favIconRR); - - // 2. Simulate user action - // - // - Navigate: user left-clicks on link - // - SaveLink: user right-clicks on link and chooses "save link as" from menu - QTRY_VERIFY(view.focusWidget()); - QWidget *renderWidget = view.focusWidget(); - QPoint linkPos(10, 10); - if (userAction == SaveLink) { - view.setContextMenuPolicy(Qt::CustomContextMenu); - auto event1 = new QContextMenuEvent(QContextMenuEvent::Mouse, linkPos); - auto event2 = new QMouseEvent(QEvent::MouseButtonPress, linkPos, Qt::RightButton, {}, {}); - auto event3 = new QMouseEvent(QEvent::MouseButtonRelease, linkPos, Qt::RightButton, {}, {}); - QCoreApplication::postEvent(renderWidget, event1); - QCoreApplication::postEvent(renderWidget, event2); - QCoreApplication::postEvent(renderWidget, event3); - QVERIFY(waitForSignal(&view, &QWidget::customContextMenuRequested)); - page.triggerAction(QWebEnginePage::DownloadLinkToDisk); - } else - QTest::mouseClick(renderWidget, Qt::LeftButton, {}, linkPos); - - // 3. Deliver requested file - // - // Request/response headers vary. - auto fileRR = waitForRequest(&server); - QVERIFY(fileRR); - QCOMPARE(fileRR->requestMethod(), QByteArrayLiteral("GET")); - QCOMPARE(fileRR->requestPath(), QByteArrayLiteral("/") + fileName); - if (fileHasReferer) - QCOMPARE(fileRR->requestHeader(QByteArrayLiteral("referer")), server.url().toEncoded()); - else - QCOMPARE(fileRR->requestHeader(QByteArrayLiteral("referer")), QByteArray()); - if (!fileDisposition.isEmpty()) - fileRR->setResponseHeader(QByteArrayLiteral("content-disposition"), fileDisposition); - if (!fileMimeTypeDeclared.isEmpty()) - fileRR->setResponseHeader(QByteArrayLiteral("content-type"), fileMimeTypeDeclared); - fileRR->setResponseBody(fileContents); - fileRR->sendResponse(); - - // 4a. File is displayed and not downloaded - end test - if (fileAction == FileIsDisplayed) { - QVERIFY(waitForSignal(&page, &QWebEnginePage::loadFinished, [&](bool ok) { loadOk = ok; })); - QVERIFY(loadOk); - return; - } + QFETCH(FileAction, fileAction); + + // Set up HTTP server + int indexRequestCount = 0; + int fileRequestCount = 0; + QByteArray fileRequestReferer; + ScopedConnection sc1 = connect(m_server, &HttpServer::newRequest, [&](HttpReqRep *rr) { + if (rr->requestMethod() == "GET" && rr->requestPath() == "/") { + indexRequestCount++; + + rr->setResponseHeader(QByteArrayLiteral("content-type"), QByteArrayLiteral("text/html")); + QByteArray html; + html += QByteArrayLiteral("<html><body><a href=\""); + html += fileName; + html += QByteArrayLiteral("\" "); + if (anchorHasDownloadAttribute) + html += QByteArrayLiteral("download"); + html += QByteArrayLiteral(">Link</a></body></html>"); + rr->setResponseBody(html); + rr->sendResponse(); + } else if (rr->requestMethod() == "GET" && rr->requestPath() == "/" + fileName) { + fileRequestCount++; + fileRequestReferer = rr->requestHeader(QByteArrayLiteral("referer")); + + if (!fileDisposition.isEmpty()) + rr->setResponseHeader(QByteArrayLiteral("content-disposition"), fileDisposition); + if (!fileMimeTypeDeclared.isEmpty()) + rr->setResponseHeader(QByteArrayLiteral("content-type"), fileMimeTypeDeclared); + rr->setResponseBody(fileContents); + rr->sendResponse(); + } else { + rr->setResponseStatus(404); + rr->sendResponse(); + } + }); - // 4b. File is downloaded - check QWebEngineDownloadItem attributes + // Set up profile and download handler QTemporaryDir tmpDir; QVERIFY(tmpDir.isValid()); QByteArray slashFileName = QByteArrayLiteral("/") + fileName; QString suggestedPath = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation) + slashFileName; QString downloadPath = tmpDir.path() + slashFileName; - QUrl downloadUrl = server.url(slashFileName); - QWebEngineDownloadItem *downloadItem = nullptr; - QVERIFY(waitForSignal(&profile, &QWebEngineProfile::downloadRequested, - [&](QWebEngineDownloadItem *item) { + QUrl downloadUrl = m_server->url(slashFileName); + int acceptedCount = 0; + int finishedCount = 0; + ScopedConnection sc2 = connect(m_profile, &QWebEngineProfile::downloadRequested, [&](QWebEngineDownloadItem *item) { QCOMPARE(item->state(), QWebEngineDownloadItem::DownloadRequested); QCOMPARE(item->isFinished(), false); QCOMPARE(item->totalBytes(), -1); QCOMPARE(item->receivedBytes(), 0); QCOMPARE(item->interruptReason(), QWebEngineDownloadItem::NoReason); - QCOMPARE(item->type(), downloadType); + QCOMPARE(item->type(), expectedDownloadType(userAction, fileDisposition)); QCOMPARE(item->isSavePageDownload(), false); QCOMPARE(item->mimeType(), QString(fileMimeTypeDetected)); QCOMPARE(item->path(), suggestedPath); QCOMPARE(item->savePageFormat(), QWebEngineDownloadItem::UnknownSaveFormat); QCOMPARE(item->url(), downloadUrl); + + connect(item, &QWebEngineDownloadItem::finished, [&, item]() { + QCOMPARE(item->state(), QWebEngineDownloadItem::DownloadCompleted); + QCOMPARE(item->isFinished(), true); + QCOMPARE(item->totalBytes(), fileContents.size()); + QCOMPARE(item->receivedBytes(), fileContents.size()); + QCOMPARE(item->interruptReason(), QWebEngineDownloadItem::NoReason); + QCOMPARE(item->type(), expectedDownloadType(userAction, fileDisposition)); + QCOMPARE(item->isSavePageDownload(), false); + QCOMPARE(item->mimeType(), QString(fileMimeTypeDetected)); + QCOMPARE(item->path(), downloadPath); + QCOMPARE(item->savePageFormat(), QWebEngineDownloadItem::UnknownSaveFormat); + QCOMPARE(item->url(), downloadUrl); + + finishedCount++; + }); item->setPath(downloadPath); item->accept(); - downloadItem = item; - })); - QVERIFY(downloadItem); - bool finishOk = false; - QVERIFY(waitForSignal(downloadItem, &QWebEngineDownloadItem::finished, [&]() { - auto item = downloadItem; - QCOMPARE(item->state(), QWebEngineDownloadItem::DownloadCompleted); - QCOMPARE(item->isFinished(), true); - QCOMPARE(item->totalBytes(), fileContents.size()); - QCOMPARE(item->receivedBytes(), fileContents.size()); - QCOMPARE(item->interruptReason(), QWebEngineDownloadItem::NoReason); - QCOMPARE(item->type(), downloadType); - QCOMPARE(item->isSavePageDownload(), false); - QCOMPARE(item->mimeType(), QString(fileMimeTypeDetected)); - QCOMPARE(item->path(), downloadPath); - QCOMPARE(item->savePageFormat(), QWebEngineDownloadItem::UnknownSaveFormat); - QCOMPARE(item->url(), downloadUrl); - finishOk = true; - })); - QVERIFY(finishOk); - // 5. Check actual file contents + acceptedCount++; + }); + + // Load an HTML page with a link + // + // The only variation being whether the <a> element has a "download" + // attribute or not. + QSignalSpy loadSpy(m_page, &QWebEnginePage::loadFinished); + m_view->load(m_server->url()); + QTRY_COMPARE(loadSpy.count(), 1); + QCOMPARE(loadSpy.takeFirst().value(0).toBool(), true); + QCOMPARE(indexRequestCount, 1); + + simulateUserAction(QPoint(10, 10), userAction); + + // If file is expected to be displayed and not downloaded then end test + if (fileAction == FileIsDisplayed) { + QTRY_COMPARE(loadSpy.count(), 1); + QCOMPARE(loadSpy.takeFirst().value(0).toBool(), true); + QCOMPARE(acceptedCount, 0); + return; + } + + // Otherwise file is downloaded + QTRY_COMPARE(acceptedCount, 1); + QTRY_COMPARE(finishedCount, 1); + QCOMPARE(fileRequestCount, 1); + if (fileHasReferer) + QCOMPARE(fileRequestReferer, m_server->url().toEncoded()); + else + QCOMPARE(fileRequestReferer, QByteArray()); QFile file(downloadPath); QVERIFY(file.open(QIODevice::ReadOnly)); QCOMPARE(file.readAll(), fileContents); } +void tst_QWebEngineDownloads::downloadTwoLinks_data() +{ + QTest::addColumn<UserAction>("action1"); + QTest::addColumn<UserAction>("action2"); + QTest::newRow("Save+Save") << SaveLink << SaveLink; + QTest::newRow("Save+Click") << SaveLink << ClickLink; + QTest::newRow("Click+Save") << ClickLink << SaveLink; + QTest::newRow("Click+Click") << ClickLink << ClickLink; +} + void tst_QWebEngineDownloads::downloadTwoLinks() { - HttpServer server; - QSignalSpy requestSpy(&server, &HttpServer::newRequest); - QList<HttpReqRep*> results; - connect(&server, &HttpServer::newRequest, [&](HttpReqRep *rr) { - rr->setParent(nullptr); - results.append(rr); + QFETCH(UserAction, action1); + QFETCH(UserAction, action2); + + // Set up HTTP server + int file1RequestCount = 0; + int file2RequestCount = 0; + ScopedConnection sc1 = connect(m_server, &HttpServer::newRequest, [&](HttpReqRep *rr) { + if (rr->requestMethod() == "GET" && rr->requestPath() == "/") { + rr->setResponseHeader(QByteArrayLiteral("content-type"), QByteArrayLiteral("text/html")); + rr->setResponseBody(QByteArrayLiteral("<html><body><a href=\"file1\" download>Link1</a><br/><a href=\"file2\">Link2</a></body></html>")); + rr->sendResponse(); + } else if (rr->requestMethod() == "GET" && rr->requestPath() == "/file1") { + file1RequestCount++; + rr->setResponseHeader(QByteArrayLiteral("content-type"), QByteArrayLiteral("text/plain")); + rr->setResponseBody(QByteArrayLiteral("file1")); + rr->sendResponse(); + } else if (rr->requestMethod() == "GET" && rr->requestPath() == "/file2") { + file2RequestCount++; + rr->setResponseHeader(QByteArrayLiteral("content-type"), QByteArrayLiteral("text/plain")); + rr->setResponseHeader(QByteArrayLiteral("content-disposition"), QByteArrayLiteral("attachment")); + rr->setResponseBody(QByteArrayLiteral("file2")); + rr->sendResponse(); + } else { + rr->setResponseStatus(404); + rr->sendResponse(); + } }); + // Set up profile and download handler QTemporaryDir tmpDir; QVERIFY(tmpDir.isValid()); QString standardDir = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation); - - QWebEngineProfile profile; - profile.setHttpCacheType(QWebEngineProfile::NoCache); - QList<QPointer<QWebEngineDownloadItem>> downloadItems; - connect(&profile, &QWebEngineProfile::downloadRequested, [&](QWebEngineDownloadItem *item) { + int acceptedCount = 0; + int finishedCount = 0; + ScopedConnection sc2 = connect(m_profile, &QWebEngineProfile::downloadRequested, [&](QWebEngineDownloadItem *item) { QCOMPARE(item->state(), QWebEngineDownloadItem::DownloadRequested); QCOMPARE(item->isFinished(), false); QCOMPARE(item->totalBytes(), -1); QCOMPARE(item->receivedBytes(), 0); QCOMPARE(item->interruptReason(), QWebEngineDownloadItem::NoReason); + QCOMPARE(item->savePageFormat(), QWebEngineDownloadItem::UnknownSaveFormat); + QCOMPARE(item->mimeType(), QStringLiteral("text/plain")); QString filePart = QChar('/') + item->url().fileName(); QCOMPARE(item->path(), standardDir + filePart); + + // type() is broken due to race condition in DownloadManagerDelegateQt + if (action1 == ClickLink && action2 == ClickLink) { + if (filePart == QStringLiteral("/file1")) + QCOMPARE(item->type(), expectedDownloadType(action1)); + else if (filePart == QStringLiteral("/file2")) + QCOMPARE(item->type(), expectedDownloadType(action2, QByteArrayLiteral("attachment"))); + else + QFAIL(qPrintable("Unexpected file name: " + filePart)); + } + + connect(item, &QWebEngineDownloadItem::finished, [&]() { + finishedCount++; + }); item->setPath(tmpDir.path() + filePart); item->accept(); - downloadItems.append(item); + + acceptedCount++; }); - QWebEnginePage page(&profile); - QWebEngineView view; - view.setPage(&page); - - view.load(server.url()); - view.show(); - QTRY_COMPARE(requestSpy.count(), 1); - std::unique_ptr<HttpReqRep> indexRR(results.takeFirst()); - QVERIFY(indexRR); - QCOMPARE(indexRR->requestMethod(), QByteArrayLiteral("GET")); - QCOMPARE(indexRR->requestPath(), QByteArrayLiteral("/")); - indexRR->setResponseHeader(QByteArrayLiteral("content-type"), QByteArrayLiteral("text/html")); - indexRR->setResponseBody(QByteArrayLiteral("<html><body><a href=\"file1\" download>Link1</a><br/><a href=\"file2\">Link2</a></body></html>")); - indexRR->sendResponse(); - bool loadOk = false; - QVERIFY(waitForSignal(&page, &QWebEnginePage::loadFinished, [&](bool ok){ loadOk = ok; })); - QVERIFY(loadOk); - - QTRY_COMPARE(requestSpy.count(), 2); - std::unique_ptr<HttpReqRep> favIconRR(results.takeFirst()); - QVERIFY(favIconRR); - favIconRR->setResponseStatus(404); - favIconRR->sendResponse(); - - QTRY_VERIFY(view.focusWidget()); - QWidget *renderWidget = view.focusWidget(); - QTest::mouseClick(renderWidget, Qt::LeftButton, {}, QPoint(10, 10)); - QTest::mouseClick(renderWidget, Qt::LeftButton, {}, QPoint(10, 30)); - - QTRY_VERIFY(requestSpy.count() >= 3); - std::unique_ptr<HttpReqRep> file1RR(results.takeFirst()); - QVERIFY(file1RR); - QCOMPARE(file1RR->requestMethod(), QByteArrayLiteral("GET")); - QTRY_COMPARE(requestSpy.count(), 4); - std::unique_ptr<HttpReqRep> file2RR(results.takeFirst()); - QVERIFY(file2RR); - QCOMPARE(file2RR->requestMethod(), QByteArrayLiteral("GET")); - - // Handle one request overtaking the other - if (file1RR->requestPath() == QByteArrayLiteral("/file2")) - std::swap(file1RR, file2RR); - - QCOMPARE(file1RR->requestPath(), QByteArrayLiteral("/file1")); - QCOMPARE(file2RR->requestPath(), QByteArrayLiteral("/file2")); - - file1RR->setResponseHeader(QByteArrayLiteral("content-type"), QByteArrayLiteral("text/plain")); - file1RR->setResponseBody(QByteArrayLiteral("file1")); - file1RR->sendResponse(); - file2RR->setResponseHeader(QByteArrayLiteral("content-type"), QByteArrayLiteral("text/plain")); - file2RR->setResponseHeader(QByteArrayLiteral("content-disposition"), QByteArrayLiteral("attachment")); - file2RR->setResponseBody(QByteArrayLiteral("file2")); - file2RR->sendResponse(); - - // Now wait for downloadRequested signals: - QTRY_VERIFY(downloadItems.count() >= 2); - QScopedPointer<QWebEngineDownloadItem> item1(downloadItems.takeFirst()); - QScopedPointer<QWebEngineDownloadItem> item2(downloadItems.takeFirst()); - QVERIFY(item1); - QVERIFY(item2); - - // Handle one request overtaking the other - if (item1->url().fileName() == QByteArrayLiteral("file2")) - qSwap(item1, item2); - - QCOMPARE(item1->type(), QWebEngineDownloadItem::DownloadAttribute); - QCOMPARE(item1->mimeType(), QStringLiteral("text/plain")); - QCOMPARE(item1->savePageFormat(), QWebEngineDownloadItem::UnknownSaveFormat); - QCOMPARE(item1->url(), server.url(QByteArrayLiteral("/file1"))); - QTRY_COMPARE(item1->state(), QWebEngineDownloadItem::DownloadCompleted); - - QCOMPARE(item2->type(), QWebEngineDownloadItem::Attachment); - QCOMPARE(item2->mimeType(), QStringLiteral("text/plain")); - QCOMPARE(item2->savePageFormat(), QWebEngineDownloadItem::UnknownSaveFormat); - QCOMPARE(item2->url(), server.url(QByteArrayLiteral("/file2"))); - QTRY_COMPARE(item2->state(), QWebEngineDownloadItem::DownloadCompleted); + QSignalSpy loadSpy(m_page, &QWebEnginePage::loadFinished); + m_view->load(m_server->url()); + QTRY_COMPARE(loadSpy.count(), 1); + QCOMPARE(loadSpy.takeFirst().value(0).toBool(), true); + + // Trigger downloads + simulateUserAction(QPoint(10, 10), action1); + simulateUserAction(QPoint(10, 30), action2); + + // Wait for downloads + if (action1 == action2 && action1 == ClickLink) { + // With two clicks, sometimes both files get downloaded, sometimes only + // the second file, depending on the timing. This is expected and + // follows Chromium's behavior. We check here only that the second file + // is downloaded correctly (and that we do not crash). + // + // The first download may be aborted before or after the HTTP request is + // made. In the latter case we will have both a file1 and a file2 + // request, but still only one accepted download. + QTRY_COMPARE(file2RequestCount, 1); + QTRY_VERIFY(acceptedCount >= 1); + QTRY_VERIFY(finishedCount >= 1); + QTRY_COMPARE(m_downloads.count(), 0); + } else { + // Otherwise both files should always be downloaded correctly. + QTRY_COMPARE(file1RequestCount, 1); + QTRY_COMPARE(file2RequestCount, 1); + QTRY_COMPARE(acceptedCount, 2); + QTRY_COMPARE(finishedCount, 2); + } } void tst_QWebEngineDownloads::downloadPage_data() @@ -552,37 +617,28 @@ void tst_QWebEngineDownloads::downloadPage() { QFETCH(QWebEngineDownloadItem::SavePageFormat, savePageFormat); - HttpServer server; - QWebEngineProfile profile; - QWebEnginePage page(&profile); - QWebEngineView view; - view.setPage(&page); - - view.load(server.url()); - view.show(); - auto indexRR = waitForRequest(&server); - QVERIFY(indexRR); - QCOMPARE(indexRR->requestMethod(), QByteArrayLiteral("GET")); - QCOMPARE(indexRR->requestPath(), QByteArrayLiteral("/")); - indexRR->setResponseHeader(QByteArrayLiteral("content-type"), QByteArrayLiteral("text/html")); - indexRR->setResponseBody(QByteArrayLiteral("<html><body>Hello</body></html>")); - indexRR->sendResponse(); - bool loadOk = false; - QVERIFY(waitForSignal(&page, &QWebEnginePage::loadFinished, [&](bool ok){ loadOk = ok; })); - QVERIFY(loadOk); - - auto favIconRR = waitForFaviconRequest(&server); - QVERIFY(favIconRR); + // Set up HTTP server + int indexRequestCount = 0; + ScopedConnection sc1 = connect(m_server, &HttpServer::newRequest, [&](HttpReqRep *rr) { + if (rr->requestMethod() == "GET" && rr->requestPath() == "/") { + indexRequestCount++; + rr->setResponseHeader(QByteArrayLiteral("content-type"), QByteArrayLiteral("text/html")); + rr->setResponseBody(QByteArrayLiteral("<html><body>Hello</body></html>")); + rr->sendResponse(); + } else { + rr->setResponseStatus(404); + rr->sendResponse(); + } + }); + // Set up profile and download handler QTemporaryDir tmpDir; QVERIFY(tmpDir.isValid()); QString downloadPath = tmpDir.path() + QStringLiteral("/test.html"); - page.save(downloadPath, savePageFormat); - - QWebEngineDownloadItem *downloadItem = nullptr; - QUrl downloadUrl = server.url("/"); - QVERIFY(waitForSignal(&profile, &QWebEngineProfile::downloadRequested, - [&](QWebEngineDownloadItem *item) { + QUrl downloadUrl = m_server->url("/"); + int acceptedCount = 0; + int finishedCount = 0; + ScopedConnection sc2 = connect(m_profile, &QWebEngineProfile::downloadRequested, [&](QWebEngineDownloadItem *item) { QCOMPARE(item->state(), QWebEngineDownloadItem::DownloadInProgress); QCOMPARE(item->isFinished(), false); QCOMPARE(item->totalBytes(), -1); @@ -590,33 +646,43 @@ void tst_QWebEngineDownloads::downloadPage() QCOMPARE(item->interruptReason(), QWebEngineDownloadItem::NoReason); QCOMPARE(item->type(), QWebEngineDownloadItem::SavePage); QCOMPARE(item->isSavePageDownload(), true); - // FIXME why is mimeType always the same? + // FIXME(juvaldma): why is mimeType always the same? QCOMPARE(item->mimeType(), QStringLiteral("application/x-mimearchive")); QCOMPARE(item->path(), downloadPath); QCOMPARE(item->savePageFormat(), savePageFormat); QCOMPARE(item->url(), downloadUrl); // no need to call item->accept() - downloadItem = item; - })); - QVERIFY(downloadItem); - bool finishOk = false; - QVERIFY(waitForSignal(downloadItem, &QWebEngineDownloadItem::finished, [&]() { - auto item = downloadItem; - QCOMPARE(item->state(), QWebEngineDownloadItem::DownloadCompleted); - QCOMPARE(item->isFinished(), true); - QCOMPARE(item->totalBytes(), item->receivedBytes()); - QVERIFY(item->receivedBytes() > 0); - QCOMPARE(item->interruptReason(), QWebEngineDownloadItem::NoReason); - QCOMPARE(item->type(), QWebEngineDownloadItem::SavePage); - QCOMPARE(item->isSavePageDownload(), true); - QCOMPARE(item->mimeType(), QStringLiteral("application/x-mimearchive")); - QCOMPARE(item->path(), downloadPath); - QCOMPARE(item->savePageFormat(), savePageFormat); - QCOMPARE(item->url(), downloadUrl); - finishOk = true; - })); - QVERIFY(finishOk); + connect(item, &QWebEngineDownloadItem::finished, [&, item]() { + QCOMPARE(item->state(), QWebEngineDownloadItem::DownloadCompleted); + QCOMPARE(item->isFinished(), true); + QCOMPARE(item->totalBytes(), item->receivedBytes()); + QVERIFY(item->receivedBytes() > 0); + QCOMPARE(item->interruptReason(), QWebEngineDownloadItem::NoReason); + QCOMPARE(item->type(), QWebEngineDownloadItem::SavePage); + QCOMPARE(item->isSavePageDownload(), true); + QCOMPARE(item->mimeType(), QStringLiteral("application/x-mimearchive")); + QCOMPARE(item->path(), downloadPath); + QCOMPARE(item->savePageFormat(), savePageFormat); + QCOMPARE(item->url(), downloadUrl); + + finishedCount++; + }); + + acceptedCount++; + }); + + // Load some HTML + QSignalSpy loadSpy(m_page, &QWebEnginePage::loadFinished); + m_page->load(m_server->url()); + QTRY_COMPARE(loadSpy.count(), 1); + QCOMPARE(loadSpy.takeFirst().value(0).toBool(), true); + QCOMPARE(indexRequestCount, 1); + + // Save some HTML + m_page->save(downloadPath, savePageFormat); + QTRY_COMPARE(acceptedCount, 1); + QTRY_COMPARE(finishedCount, 1); QFile file(downloadPath); QVERIFY(file.exists()); } @@ -626,31 +692,33 @@ void tst_QWebEngineDownloads::downloadViaSetUrl() // Reproduce the scenario described in QTBUG-63388 by triggering downloads // of the same file multiple times via QWebEnginePage::setUrl - HttpServer server; - QWebEngineProfile profile; - QWebEnginePage page(&profile); - QSignalSpy loadSpy(&page, &QWebEnginePage::loadFinished); - QSignalSpy urlSpy(&page, &QWebEnginePage::urlChanged); - const QUrl indexUrl = server.url(); - const QUrl fileUrl = server.url(QByteArrayLiteral("/file")); - - // Set up the test scenario by trying to load some unrelated HTML. - - page.setUrl(indexUrl); - - auto indexRR = waitForRequest(&server); - QVERIFY(indexRR); - QCOMPARE(indexRR->requestMethod(), QByteArrayLiteral("GET")); - QCOMPARE(indexRR->requestPath(), QByteArrayLiteral("/")); - indexRR->setResponseHeader(QByteArrayLiteral("content-type"), QByteArrayLiteral("text/html")); - indexRR->setResponseBody(QByteArrayLiteral("<html><body>Hello</body></html>")); - indexRR->sendResponse(); + // Set up HTTP server + ScopedConnection sc1 = connect(m_server, &HttpServer::newRequest, [&](HttpReqRep *rr) { + if (rr->requestMethod() == "GET" && rr->requestPath() == "/") { + rr->setResponseHeader(QByteArrayLiteral("content-type"), QByteArrayLiteral("text/html")); + rr->setResponseBody(QByteArrayLiteral("<html><body>Hello</body></html>")); + rr->sendResponse(); + } else if (rr->requestMethod() == "GET" && rr->requestPath() == "/file") { + rr->setResponseHeader(QByteArrayLiteral("content-disposition"), QByteArrayLiteral("attachment")); + rr->setResponseBody(QByteArrayLiteral("redacted")); + rr->sendResponse(); + } else { + rr->setResponseStatus(404); + rr->sendResponse(); + } + }); - auto indexFavRR = waitForFaviconRequest(&server); - QVERIFY(indexFavRR); - indexRR.reset(); - indexFavRR.reset(); + // Set up profile and download handler + QVector<QUrl> downloadUrls; + ScopedConnection sc2 = connect(m_profile, &QWebEngineProfile::downloadRequested, [&](QWebEngineDownloadItem *item) { + downloadUrls.append(item->url()); + }); + // Set up the test scenario by trying to load some unrelated HTML. + QSignalSpy loadSpy(m_page, &QWebEnginePage::loadFinished); + QSignalSpy urlSpy(m_page, &QWebEnginePage::urlChanged); + const QUrl indexUrl = m_server->url(); + m_page->setUrl(indexUrl); QTRY_COMPARE(loadSpy.count(), 1); QTRY_COMPARE(urlSpy.count(), 1); QCOMPARE(loadSpy.takeFirst().value(0).toBool(), true); @@ -658,37 +726,18 @@ void tst_QWebEngineDownloads::downloadViaSetUrl() // Download files via setUrl. With QTBUG-63388 after the first iteration the // downloads would be triggered for indexUrl and not fileUrl. - - QVector<QUrl> downloadUrls; - QObject::connect(&profile, &QWebEngineProfile::downloadRequested, [&](QWebEngineDownloadItem *item) { - downloadUrls.append(item->url()); - }); - + const QUrl fileUrl = m_server->url(QByteArrayLiteral("/file")); for (int i = 0; i != 3; ++i) { - page.setUrl(fileUrl); - QCOMPARE(page.url(), fileUrl); - - auto fileRR = waitForRequest(&server); - QVERIFY(fileRR); - QCOMPARE(fileRR->requestMethod(), QByteArrayLiteral("GET")); - QCOMPARE(fileRR->requestPath(), QByteArrayLiteral("/file")); - fileRR->setResponseHeader(QByteArrayLiteral("content-disposition"), QByteArrayLiteral("attachment")); - fileRR->setResponseBody(QByteArrayLiteral("redacted")); - fileRR->sendResponse(); - -// Since 63 we no longer get favicon requests here: -// auto fileFavRR = waitForFaviconRequest(&server); -// QVERIFY(fileFavRR); - + m_page->setUrl(fileUrl); + QCOMPARE(m_page->url(), fileUrl); QTRY_COMPARE(loadSpy.count(), 1); QTRY_COMPARE(urlSpy.count(), 2); QTRY_COMPARE(downloadUrls.count(), 1); - fileRR.reset(); QCOMPARE(loadSpy.takeFirst().value(0).toBool(), false); QCOMPARE(urlSpy.takeFirst().value(0).toUrl(), fileUrl); QCOMPARE(urlSpy.takeFirst().value(0).toUrl(), indexUrl); QCOMPARE(downloadUrls.takeFirst(), fileUrl); - QCOMPARE(page.url(), indexUrl); + QCOMPARE(m_page->url(), indexUrl); } } @@ -696,26 +745,22 @@ void tst_QWebEngineDownloads::downloadFileNot1() { // Trigger file download via download() but don't accept(). - HttpServer server; - QWebEngineProfile profile; - QWebEnginePage page(&profile); - const auto filePath = QByteArrayLiteral("/file"); - const auto fileUrl = server.url(filePath); - - page.download(fileUrl); - auto fileRR = waitForRequest(&server); - QVERIFY(fileRR); - QCOMPARE(fileRR->requestMethod(), QByteArrayLiteral("GET")); - QCOMPARE(fileRR->requestPath(), filePath); - fileRR->sendResponse(); + ScopedConnection sc1 = connect(m_server, &HttpServer::newRequest, [&](HttpReqRep *rr) { + rr->setResponseStatus(404); + rr->sendResponse(); + }); QPointer<QWebEngineDownloadItem> downloadItem; - QVERIFY(waitForSignal(&profile, &QWebEngineProfile::downloadRequested, - [&](QWebEngineDownloadItem *item) { + int downloadCount = 0; + ScopedConnection sc2 = connect(m_profile, &QWebEngineProfile::downloadRequested, [&](QWebEngineDownloadItem *item) { QVERIFY(item); QCOMPARE(item->state(), QWebEngineDownloadItem::DownloadRequested); downloadItem = item; - })); + downloadCount++; + }); + + m_page->download(m_server->url(QByteArrayLiteral("/file"))); + QTRY_COMPARE(downloadCount, 1); QVERIFY(!downloadItem); } @@ -723,27 +768,23 @@ void tst_QWebEngineDownloads::downloadFileNot2() { // Trigger file download via download() but call cancel() instead of accept(). - HttpServer server; - QWebEngineProfile profile; - QWebEnginePage page(&profile); - const auto filePath = QByteArrayLiteral("/file"); - const auto fileUrl = server.url(filePath); - - page.download(fileUrl); - auto fileRR = waitForRequest(&server); - QVERIFY(fileRR); - QCOMPARE(fileRR->requestMethod(), QByteArrayLiteral("GET")); - QCOMPARE(fileRR->requestPath(), filePath); - fileRR->sendResponse(); + ScopedConnection sc1 = connect(m_server, &HttpServer::newRequest, [&](HttpReqRep *rr) { + rr->setResponseStatus(404); + rr->sendResponse(); + }); QPointer<QWebEngineDownloadItem> downloadItem; - QVERIFY(waitForSignal(&profile, &QWebEngineProfile::downloadRequested, - [&](QWebEngineDownloadItem *item) { + int downloadCount = 0; + ScopedConnection sc2 = connect(m_profile, &QWebEngineProfile::downloadRequested, [&](QWebEngineDownloadItem *item) { QVERIFY(item); QCOMPARE(item->state(), QWebEngineDownloadItem::DownloadRequested); item->cancel(); downloadItem = item; - })); + downloadCount++; + }); + + m_page->download(m_server->url(QByteArrayLiteral("/file"))); + QTRY_COMPARE(downloadCount, 1); QVERIFY(downloadItem); QCOMPARE(downloadItem->state(), QWebEngineDownloadItem::DownloadCancelled); } diff --git a/tests/auto/widgets/qwebenginepage/tst_qwebenginepage.cpp b/tests/auto/widgets/qwebenginepage/tst_qwebenginepage.cpp index 6b729d8f214acfae0036f5dfa53047a01aec9718..81877657b2e74d259080692b8208edabcd0841d3 100644 --- a/tests/auto/widgets/qwebenginepage/tst_qwebenginepage.cpp +++ b/tests/auto/widgets/qwebenginepage/tst_qwebenginepage.cpp @@ -4247,21 +4247,26 @@ void tst_QWebEnginePage::registerProtocolHandler() QFETCH(bool, permission); HttpServer server; + int mailRequestCount = 0; + connect(&server, &HttpServer::newRequest, [&](HttpReqRep *rr) { + if (rr->requestMethod() == "GET" && rr->requestPath() == "/") { + rr->setResponseBody(QByteArrayLiteral("<html><body><a id=\"link\" href=\"mailto:foo@bar.com\">some text here</a></body></html>")); + rr->sendResponse(); + } else if (rr->requestMethod() == "GET" && rr->requestPath() == "/mail?uri=mailto%3Afoo%40bar.com") { + mailRequestCount++; + rr->sendResponse(); + } else { + rr->setResponseStatus(404); + rr->sendResponse(); + } + }); + QVERIFY(server.start()); + QWebEnginePage page; QSignalSpy loadSpy(&page, &QWebEnginePage::loadFinished); QSignalSpy permissionSpy(&page, &QWebEnginePage::registerProtocolHandlerPermissionRequested); page.setUrl(server.url("/")); - auto rr1 = waitForRequest(&server); - QVERIFY(rr1); - rr1->setResponseBody(QByteArrayLiteral("<html><body><a id=\"link\" href=\"mailto:foo@bar.com\">some text here</a></body></html>")); - rr1->sendResponse(); - auto rr2 = waitForRequest(&server); - QVERIFY(rr2); - QCOMPARE(rr2->requestMethod(), QByteArrayLiteral("GET")); - QCOMPARE(rr2->requestPath(), QByteArrayLiteral("/favicon.ico")); - rr2->setResponseStatus(404); - rr2->sendResponse(); QTRY_COMPARE(loadSpy.count(), 1); QCOMPARE(loadSpy.takeFirst().value(0).toBool(), true); @@ -4283,17 +4288,10 @@ void tst_QWebEnginePage::registerProtocolHandler() page.runJavaScript(QStringLiteral("document.getElementById(\"link\").click()")); - std::unique_ptr<HttpReqRep> rr3; - if (permission) { - rr3 = waitForRequest(&server); - QVERIFY(rr3); - QCOMPARE(rr3->requestMethod(), QByteArrayLiteral("GET")); - QCOMPARE(rr3->requestPath(), QByteArrayLiteral("/mail?uri=mailto%3Afoo%40bar.com")); - rr3->sendResponse(); - } - QTRY_COMPARE(loadSpy.count(), 1); QCOMPARE(loadSpy.takeFirst().value(0).toBool(), permission); + QCOMPARE(mailRequestCount, permission ? 1 : 0); + QVERIFY(server.stop()); } void tst_QWebEnginePage::dataURLFragment()