From 1c7a520edfb850cc34a29d96417c475a373ed355 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCri=20Valdmann?= Date: Thu, 10 Aug 2017 12:24:27 +0200 Subject: Add test for downloading a link via user action Change-Id: Ide3294840ceb3d18da0c4da92d892ff467a9b739 Reviewed-by: Michal Klocek --- tests/auto/shared/http.pri | 3 + tests/auto/shared/httpreqrep.cpp | 91 +++++ tests/auto/shared/httpreqrep.h | 75 ++++ tests/auto/shared/httpserver.cpp | 70 ++++ tests/auto/shared/httpserver.h | 61 +++ tests/auto/shared/waitforsignal.h | 90 +++++ .../qwebenginedownloads/qwebenginedownloads.pro | 3 + .../tst_qwebenginedownloads.cpp | 418 +++++++++++++++++++++ tests/auto/widgets/widgets.pro | 4 +- 9 files changed, 814 insertions(+), 1 deletion(-) create mode 100644 tests/auto/shared/http.pri create mode 100644 tests/auto/shared/httpreqrep.cpp create mode 100644 tests/auto/shared/httpreqrep.h create mode 100644 tests/auto/shared/httpserver.cpp create mode 100644 tests/auto/shared/httpserver.h create mode 100644 tests/auto/shared/waitforsignal.h create mode 100644 tests/auto/widgets/qwebenginedownloads/qwebenginedownloads.pro create mode 100644 tests/auto/widgets/qwebenginedownloads/tst_qwebenginedownloads.cpp diff --git a/tests/auto/shared/http.pri b/tests/auto/shared/http.pri new file mode 100644 index 000000000..5236e9d26 --- /dev/null +++ b/tests/auto/shared/http.pri @@ -0,0 +1,3 @@ +HEADERS += $$PWD/httpserver.h $$PWD/httpreqrep.h +SOURCES += $$PWD/httpserver.cpp $$PWD/httpreqrep.cpp +INCLUDEPATH += $$PWD diff --git a/tests/auto/shared/httpreqrep.cpp b/tests/auto/shared/httpreqrep.cpp new file mode 100644 index 000000000..eb2db6890 --- /dev/null +++ b/tests/auto/shared/httpreqrep.cpp @@ -0,0 +1,91 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ +#include "httpreqrep.h" + +HttpReqRep::HttpReqRep(QTcpSocket *socket, QObject *parent) + : QObject(parent), m_socket(socket) +{ + m_socket->setParent(this); + connect(m_socket, &QIODevice::readyRead, this, &HttpReqRep::handleReadyRead); +} + +void HttpReqRep::sendResponse() +{ + m_socket->write("HTTP/1.1 "); + m_socket->write(QByteArray::number(m_responseStatusCode)); + m_socket->write(" OK?\r\n"); + for (const auto & kv : m_responseHeaders) { + m_socket->write(kv.first); + m_socket->write(": "); + m_socket->write(kv.second); + m_socket->write("\r\n"); + } + m_socket->write("\r\n"); + m_socket->write(m_responseBody); + m_socket->close(); +} + +QByteArray HttpReqRep::requestHeader(const QByteArray &key) const +{ + auto it = m_requestHeaders.find(key); + if (it != m_requestHeaders.end()) + return it->second; + return {}; +} + +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")) + break; + int colonIndex = headerLine.indexOf(':'); + if (colonIndex < 0) { + qWarning("HttpReqRep: invalid header line"); + Q_EMIT readFinished(false); + 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); +} diff --git a/tests/auto/shared/httpreqrep.h b/tests/auto/shared/httpreqrep.h new file mode 100644 index 000000000..4e9f10dff --- /dev/null +++ b/tests/auto/shared/httpreqrep.h @@ -0,0 +1,75 @@ +/**************************************************************************** +** +** 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 HTTPREQREP_H +#define HTTPREQREP_H + +#include + +#include + +// Represents an HTTP request-response exchange. +class HttpReqRep : public QObject +{ + Q_OBJECT +public: + HttpReqRep(QTcpSocket *socket, QObject *parent = nullptr); + void sendResponse(); + QByteArray requestMethod() const { return m_requestMethod; } + QByteArray requestPath() const { return m_requestPath; } + QByteArray requestHeader(const QByteArray &key) const; + void setResponseStatus(int statusCode) + { + m_responseStatusCode = statusCode; + } + void setResponseHeader(const QByteArray &key, QByteArray value) + { + m_responseHeaders[key.toLower()] = std::move(value); + } + void setResponseBody(QByteArray content) + { + m_responseHeaders["content-length"] = QByteArray::number(content.size()); + m_responseBody = std::move(content); + } + +Q_SIGNALS: + void readFinished(bool ok); + +private Q_SLOTS: + void handleReadyRead(); + +private: + QTcpSocket *m_socket = nullptr; + QByteArray m_requestMethod; + QByteArray m_requestPath; + std::map m_requestHeaders; + int m_responseStatusCode = 200; + std::map m_responseHeaders; + QByteArray m_responseBody; +}; + +#endif // !HTTPREQREP_H diff --git a/tests/auto/shared/httpserver.cpp b/tests/auto/shared/httpserver.cpp new file mode 100644 index 000000000..6012379f2 --- /dev/null +++ b/tests/auto/shared/httpserver.cpp @@ -0,0 +1,70 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ +#include "httpserver.h" + +#include "waitforsignal.h" + +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 +{ + auto copy = m_url; + copy.setPath(path); + return copy; +} + +void HttpServer::handleNewConnection() +{ + auto reqRep = new HttpReqRep(m_tcpServer.nextPendingConnection(), this); + connect(reqRep, &HttpReqRep::readFinished, this, &HttpServer::handleReadFinished); +} + +void HttpServer::handleReadFinished(bool ok) +{ + auto reqRep = qobject_cast(sender()); + if (ok) + Q_EMIT newRequest(reqRep); + else + reqRep->deleteLater(); +} + +std::unique_ptr waitForRequest(HttpServer *server) +{ + std::unique_ptr result; + waitForSignal(server, &HttpServer::newRequest, [&](HttpReqRep *rr) { + rr->setParent(nullptr); + result.reset(rr); + }); + return result; +} diff --git a/tests/auto/shared/httpserver.h b/tests/auto/shared/httpserver.h new file mode 100644 index 000000000..e45743b7b --- /dev/null +++ b/tests/auto/shared/httpserver.h @@ -0,0 +1,61 @@ +/**************************************************************************** +** +** 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 HTTPSERVER_H +#define HTTPSERVER_H + +#include "httpreqrep.h" + +#include +#include + +#include + +// Listens on a TCP socket and creates HttpReqReps for each connection. +class HttpServer : public QObject +{ + Q_OBJECT + + QTcpServer m_tcpServer; + QUrl m_url; + +public: + HttpServer(QObject *parent = nullptr); + QUrl url(const QString &path = QStringLiteral("/")) const; + +Q_SIGNALS: + void newRequest(HttpReqRep *reqRep); + +private Q_SLOTS: + void handleNewConnection(); + void handleReadFinished(bool ok); +}; + +// Wait for an HTTP request to be received. +std::unique_ptr waitForRequest(HttpServer *server); + +#endif // !HTTPSERVER_H diff --git a/tests/auto/shared/waitforsignal.h b/tests/auto/shared/waitforsignal.h new file mode 100644 index 000000000..9b2daf7af --- /dev/null +++ b/tests/auto/shared/waitforsignal.h @@ -0,0 +1,90 @@ +/**************************************************************************** +** +** 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 +#include + +#include + +// Implementation details of waitForSignal. +namespace { + // Wraps a functor to set a flag and exit from event loop if called. + template + struct waitForSignal_SignalHandlerWrapper { + waitForSignal_SignalHandlerWrapper(SignalHandler &&sh) + : signalHandler(std::forward(sh)) {} + + template + void operator()(Args && ... args) { + signalHandler(std::forward(args)...); + hasBeenCalled = true; + loop.exitLoop(); + } + + SignalHandler &&signalHandler; + QTestEventLoop loop; + bool hasBeenCalled = false; + }; + + // No-op functor. + struct waitForSignal_DefaultSignalHandler { + template + 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 +bool waitForSignal(Sender &&sender, Signal &&signal, SignalHandler &&signalHandler, int timeout = 15000) +{ + waitForSignal_SignalHandlerWrapper signalHandlerWrapper( + std::forward(signalHandler)); + auto connection = QObject::connect( + std::forward(sender), + std::forward(signal), + std::ref(signalHandlerWrapper)); + signalHandlerWrapper.loop.enterLoopMSecs(timeout); + QObject::disconnect(connection); + return signalHandlerWrapper.hasBeenCalled; +} + +template +bool waitForSignal(Sender &&sender, Signal &&signal, int timeout = 15000) +{ + return waitForSignal(std::forward(sender), + std::forward(signal), + waitForSignal_DefaultSignalHandler(), + timeout); +} + +#endif // !WAITFORSIGNAL_H diff --git a/tests/auto/widgets/qwebenginedownloads/qwebenginedownloads.pro b/tests/auto/widgets/qwebenginedownloads/qwebenginedownloads.pro new file mode 100644 index 000000000..18a66c466 --- /dev/null +++ b/tests/auto/widgets/qwebenginedownloads/qwebenginedownloads.pro @@ -0,0 +1,3 @@ +include(../tests.pri) +include(../../shared/http.pri) +QT *= core-private diff --git a/tests/auto/widgets/qwebenginedownloads/tst_qwebenginedownloads.cpp b/tests/auto/widgets/qwebenginedownloads/tst_qwebenginedownloads.cpp new file mode 100644 index 000000000..a03681cb1 --- /dev/null +++ b/tests/auto/widgets/qwebenginedownloads/tst_qwebenginedownloads.cpp @@ -0,0 +1,418 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class tst_QWebEngineDownloads : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void downloadLink_data(); + void downloadLink(); +}; + +enum DownloadTestUserAction { + SaveLink, + Navigate, +}; + +enum DownloadTestFileAction { + FileIsDownloaded, + FileIsDisplayed, +}; + +Q_DECLARE_METATYPE(DownloadTestUserAction); +Q_DECLARE_METATYPE(DownloadTestFileAction); + +void tst_QWebEngineDownloads::downloadLink_data() +{ + QTest::addColumn("userAction"); + QTest::addColumn("anchorHasDownloadAttribute"); + QTest::addColumn("fileName"); + QTest::addColumn("fileContents"); + QTest::addColumn("fileMimeTypeDeclared"); + QTest::addColumn("fileMimeTypeDetected"); + QTest::addColumn("fileDisposition"); + QTest::addColumn("fileHasReferer"); + QTest::addColumn("fileAction"); + QTest::addColumn("downloadType"); + + // SaveLink should always trigger a download, even for empty files. + QTest::newRow("save link to empty file") + /* userAction */ << SaveLink + /* anchorHasDownloadAttribute */ << false + /* fileName */ << QByteArrayLiteral("foo.txt") + /* fileContents */ << QByteArrayLiteral("") + /* fileMimeTypeDeclared */ << QByteArrayLiteral("") + /* fileMimeTypeDetected */ << QByteArrayLiteral("") + /* fileDisposition */ << QByteArrayLiteral("") + /* fileHasReferer */ << false + /* fileAction */ << FileIsDownloaded + /* downloadType */ << QWebEngineDownloadItem::DownloadAttribute; + + // SaveLink should always trigger a download, also for text files. + QTest::newRow("save link to text file") + /* userAction */ << SaveLink + /* anchorHasDownloadAttribute */ << false + /* fileName */ << QByteArrayLiteral("foo.txt") + /* fileContents */ << QByteArrayLiteral("bar") + /* fileMimeTypeDeclared */ << QByteArrayLiteral("text/plain") + /* fileMimeTypeDetected */ << QByteArrayLiteral("text/plain") + /* fileDisposition */ << QByteArrayLiteral("") + /* fileHasReferer */ << false + /* fileAction */ << FileIsDownloaded + /* downloadType */ << QWebEngineDownloadItem::DownloadAttribute; + + // ... adding the "download" attribute should have no effect. + QTest::newRow("save link to text file (attribute)") + /* userAction */ << SaveLink + /* anchorHasDownloadAttribute */ << true + /* fileName */ << QByteArrayLiteral("foo.txt") + /* fileContents */ << QByteArrayLiteral("bar") + /* fileMimeTypeDeclared */ << QByteArrayLiteral("text/plain") + /* fileMimeTypeDetected */ << QByteArrayLiteral("text/plain") + /* fileDisposition */ << QByteArrayLiteral("") + /* fileHasReferer */ << false + /* fileAction */ << FileIsDownloaded + /* downloadType */ << QWebEngineDownloadItem::DownloadAttribute; + + // ... adding the "attachment" content disposition should also have no effect. + QTest::newRow("save link to text file (attachment)") + /* userAction */ << SaveLink + /* anchorHasDownloadAttribute */ << false + /* fileName */ << QByteArrayLiteral("foo.txt") + /* fileContents */ << QByteArrayLiteral("bar") + /* fileMimeTypeDeclared */ << QByteArrayLiteral("text/plain") + /* fileMimeTypeDetected */ << QByteArrayLiteral("text/plain") + /* fileDisposition */ << QByteArrayLiteral("attachment") + /* fileHasReferer */ << false + /* fileAction */ << FileIsDownloaded + /* downloadType */ << QWebEngineDownloadItem::UserRequested; + + // ... even adding both should have no effect. + QTest::newRow("save link to text file (attribute+attachment)") + /* userAction */ << SaveLink + /* anchorHasDownloadAttribute */ << true + /* fileName */ << QByteArrayLiteral("foo.txt") + /* fileContents */ << QByteArrayLiteral("bar") + /* fileMimeTypeDeclared */ << QByteArrayLiteral("text/plain") + /* fileMimeTypeDetected */ << QByteArrayLiteral("text/plain") + /* fileDisposition */ << QByteArrayLiteral("attachment") + /* fileHasReferer */ << false + /* fileAction */ << FileIsDownloaded + /* downloadType */ << QWebEngineDownloadItem::UserRequested; + + // Navigating to an empty file should show an empty page. + QTest::newRow("navigate to empty file") + /* userAction */ << Navigate + /* anchorHasDownloadAttribute */ << false + /* fileName */ << QByteArrayLiteral("foo.txt") + /* fileContents */ << QByteArrayLiteral("") + /* fileMimeTypeDeclared */ << QByteArrayLiteral("") + /* fileMimeTypeDetected */ << QByteArrayLiteral("") + /* fileDisposition */ << QByteArrayLiteral("") + /* fileHasReferer */ << true + /* fileAction */ << FileIsDisplayed + /* downloadType */ << /* unused */ QWebEngineDownloadItem::DownloadAttribute; + + // Navigating to a text file should show the text file. + QTest::newRow("navigate to text file") + /* userAction */ << Navigate + /* anchorHasDownloadAttribute */ << false + /* fileName */ << QByteArrayLiteral("foo.txt") + /* fileContents */ << QByteArrayLiteral("bar") + /* fileMimeTypeDeclared */ << QByteArrayLiteral("text/plain") + /* fileMimeTypeDetected */ << QByteArrayLiteral("text/plain") + /* fileDisposition */ << QByteArrayLiteral("") + /* fileHasReferer */ << true + /* fileAction */ << FileIsDisplayed + /* downloadType */ << /* unused */ QWebEngineDownloadItem::DownloadAttribute; + + // ... unless the link has the "download" attribute: then the file should be downloaded. + QTest::newRow("navigate to text file (attribute)") + /* userAction */ << Navigate + /* anchorHasDownloadAttribute */ << true + /* fileName */ << QByteArrayLiteral("foo.txt") + /* fileContents */ << QByteArrayLiteral("bar") + /* fileMimeTypeDeclared */ << QByteArrayLiteral("text/plain") + /* fileMimeTypeDetected */ << QByteArrayLiteral("text/plain") + /* fileDisposition */ << QByteArrayLiteral("") + /* fileHasReferer */ << false + /* fileAction */ << FileIsDownloaded + /* downloadType */ << QWebEngineDownloadItem::DownloadAttribute; + + // ... same with the content disposition header save for the download type. + QTest::newRow("navigate to text file (attachment)") + /* userAction */ << Navigate + /* anchorHasDownloadAttribute */ << false + /* fileName */ << QByteArrayLiteral("foo.txt") + /* fileContents */ << QByteArrayLiteral("bar") + /* fileMimeTypeDeclared */ << QByteArrayLiteral("text/plain") + /* fileMimeTypeDetected */ << QByteArrayLiteral("text/plain") + /* fileDisposition */ << QByteArrayLiteral("attachment") + /* fileHasReferer */ << true + /* fileAction */ << FileIsDownloaded + /* downloadType */ << QWebEngineDownloadItem::Attachment; + + // ... and both. + QTest::newRow("navigate to text file (attribute+attachment)") + /* userAction */ << Navigate + /* anchorHasDownloadAttribute */ << true + /* fileName */ << QByteArrayLiteral("foo.txt") + /* fileContents */ << QByteArrayLiteral("bar") + /* fileMimeTypeDeclared */ << QByteArrayLiteral("text/plain") + /* fileMimeTypeDetected */ << QByteArrayLiteral("text/plain") + /* fileDisposition */ << QByteArrayLiteral("attachment") + /* fileHasReferer */ << false + /* fileAction */ << FileIsDownloaded + /* downloadType */ << QWebEngineDownloadItem::Attachment; + + // The file's extension has no effect. + QTest::newRow("navigate to supposed zip file") + /* userAction */ << Navigate + /* anchorHasDownloadAttribute */ << false + /* fileName */ << QByteArrayLiteral("foo.zip") + /* fileContents */ << QByteArrayLiteral("bar") + /* fileMimeTypeDeclared */ << QByteArrayLiteral("") + /* fileMimeTypeDetected */ << QByteArrayLiteral("") + /* fileDisposition */ << QByteArrayLiteral("") + /* fileHasReferer */ << true + /* fileAction */ << FileIsDisplayed + /* downloadType */ << /* unused */ QWebEngineDownloadItem::DownloadAttribute; + + // ... the file's mime type however does. + QTest::newRow("navigate to supposed zip file (application/zip)") + /* userAction */ << Navigate + /* anchorHasDownloadAttribute */ << false + /* fileName */ << QByteArrayLiteral("foo.zip") + /* fileContents */ << QByteArrayLiteral("bar") + /* fileMimeTypeDeclared */ << QByteArrayLiteral("application/zip") + /* fileMimeTypeDetected */ << QByteArrayLiteral("application/zip") + /* fileDisposition */ << QByteArrayLiteral("") + /* fileHasReferer */ << true + /* fileAction */ << FileIsDownloaded + /* downloadType */ << QWebEngineDownloadItem::DownloadAttribute; + + // ... but we're not very picky about the exact type. + QTest::newRow("navigate to supposed zip file (application/octet-stream)") + /* userAction */ << Navigate + /* anchorHasDownloadAttribute */ << false + /* fileName */ << QByteArrayLiteral("foo.zip") + /* fileContents */ << QByteArrayLiteral("bar") + /* fileMimeTypeDeclared */ << QByteArrayLiteral("application/octet-stream") + /* fileMimeTypeDetected */ << QByteArrayLiteral("application/octet-stream") + /* fileDisposition */ << QByteArrayLiteral("") + /* fileHasReferer */ << true + /* fileAction */ << FileIsDownloaded + /* downloadType */ << QWebEngineDownloadItem::DownloadAttribute; + + // 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 + /* anchorHasDownloadAttribute */ << false + /* fileName */ << QByteArrayLiteral("foo.zip") + /* fileContents */ << zipFile + /* fileMimeTypeDeclared */ << QByteArrayLiteral("") + /* fileMimeTypeDetected */ << QByteArrayLiteral("application/octet-stream") + /* fileDisposition */ << QByteArrayLiteral("") + /* fileHasReferer */ << true + /* fileAction */ << FileIsDownloaded + /* downloadType */ << QWebEngineDownloadItem::DownloadAttribute; + + // The mime type is not guessed automatically if provided by the server. + QTest::newRow("navigate to actual zip file (application/zip)") + /* userAction */ << Navigate + /* anchorHasDownloadAttribute */ << false + /* fileName */ << QByteArrayLiteral("foo.zip") + /* fileContents */ << zipFile + /* fileMimeTypeDeclared */ << QByteArrayLiteral("application/zip") + /* fileMimeTypeDetected */ << QByteArrayLiteral("application/zip") + /* fileDisposition */ << QByteArrayLiteral("") + /* fileHasReferer */ << true + /* fileAction */ << FileIsDownloaded + /* downloadType */ << QWebEngineDownloadItem::DownloadAttribute; +} + +void tst_QWebEngineDownloads::downloadLink() +{ + QFETCH(DownloadTestUserAction, userAction); + QFETCH(bool, anchorHasDownloadAttribute); + QFETCH(QByteArray, fileName); + QFETCH(QByteArray, fileContents); + QFETCH(QByteArray, fileMimeTypeDeclared); + QFETCH(QByteArray, fileMimeTypeDetected); + QFETCH(QByteArray, fileDisposition); + QFETCH(bool, fileHasReferer); + QFETCH(DownloadTestFileAction, fileAction); + QFETCH(QWebEngineDownloadItem::DownloadType, downloadType); + + HttpServer server; + QWebEngineProfile profile; + QWebEnginePage page(&profile); + QWebEngineView view; + view.setPage(&page); + + // 1. Load an HTML page with a link + // + // The only variation being whether the 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("Link"); + 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 = waitForRequest(&server); + QVERIFY(favIconRR); + QCOMPARE(favIconRR->requestMethod(), QByteArrayLiteral("GET")); + QCOMPARE(favIconRR->requestPath(), QByteArrayLiteral("/favicon.ico")); + favIconRR->setResponseStatus(404); + favIconRR->sendResponse(); + + // 2. Simulate user action + // + // - Navigate: user left-clicks on link + // - SaveLink: user right-clicks on link and chooses "save link as" from menu + 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; + } + + // 4b. File is downloaded - check QWebEngineDownloadItem attributes + 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) { + 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->mimeType(), QString(fileMimeTypeDetected)); + QCOMPARE(item->path(), suggestedPath); + QCOMPARE(item->savePageFormat(), QWebEngineDownloadItem::UnknownSaveFormat); + QCOMPARE(item->url(), downloadUrl); + 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->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 + QFile file(downloadPath); + QVERIFY(file.open(QIODevice::ReadOnly)); + QCOMPARE(file.readAll(), fileContents); +} + +QTEST_MAIN(tst_QWebEngineDownloads) +#include "tst_qwebenginedownloads.moc" diff --git a/tests/auto/widgets/widgets.pro b/tests/auto/widgets/widgets.pro index 90352310e..441eea0fa 100644 --- a/tests/auto/widgets/widgets.pro +++ b/tests/auto/widgets/widgets.pro @@ -3,6 +3,7 @@ TEMPLATE = subdirs SUBDIRS += \ qwebengineaccessibility \ qwebenginedefaultsurfaceformat \ + qwebenginedownloads \ qwebenginefaviconmanager \ qwebenginepage \ qwebenginehistory \ @@ -24,4 +25,5 @@ contains(WEBENGINE_CONFIG, use_spellchecker):!cross_compile { # QTBUG-60268 boot2qt: SUBDIRS -= qwebengineaccessibility qwebenginedefaultsurfaceformat \ qwebenginefaviconmanager qwebenginepage qwebenginehistory \ - qwebengineprofile qwebenginescript qwebengineview + qwebengineprofile qwebenginescript qwebengineview \ + qwebenginedownloads -- cgit v1.2.3