diff --git a/tests/auto/shared/http.pri b/tests/auto/shared/http.pri
new file mode 100644
index 0000000000000000000000000000000000000000..5236e9d26935e2ddee1cc1c5e2db9f75fb1de47b
--- /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 0000000000000000000000000000000000000000..eb2db68901ac4bb72cf2c9f45440c893c44b8a35
--- /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 0000000000000000000000000000000000000000..4e9f10dff3c5b56b3d1e2c9f3ba87c54938d35dd
--- /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 <QTcpSocket>
+
+#include <utility>
+
+// 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<QByteArray, QByteArray> m_requestHeaders;
+    int m_responseStatusCode = 200;
+    std::map<QByteArray, QByteArray> 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 0000000000000000000000000000000000000000..6012379f2a68d63039f4808656d42db551166842
--- /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<HttpReqRep *>(sender());
+    if (ok)
+        Q_EMIT newRequest(reqRep);
+    else
+        reqRep->deleteLater();
+}
+
+std::unique_ptr<HttpReqRep> waitForRequest(HttpServer *server)
+{
+    std::unique_ptr<HttpReqRep> 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 0000000000000000000000000000000000000000..e45743b7b0e19aba95e7eca43d2f176182a746d9
--- /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 <QTcpServer>
+#include <QUrl>
+
+#include <memory>
+
+// 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<HttpReqRep> 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 0000000000000000000000000000000000000000..9b2daf7af03c5033df955227517de5773927c5f7
--- /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 <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/qwebenginedownloads.pro b/tests/auto/widgets/qwebenginedownloads/qwebenginedownloads.pro
new file mode 100644
index 0000000000000000000000000000000000000000..18a66c466abe0f69b56995124f258e3d97f0b157
--- /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 0000000000000000000000000000000000000000..a03681cb1dfc8db332faedc3922a82f4d8efad99
--- /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 <QCoreApplication>
+#include <QStandardPaths>
+#include <QTemporaryDir>
+#include <QTest>
+#include <QWebEngineDownloadItem>
+#include <QWebEnginePage>
+#include <QWebEngineProfile>
+#include <QWebEngineView>
+#include <httpserver.h>
+#include <waitforsignal.h>
+
+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<DownloadTestUserAction>("userAction");
+    QTest::addColumn<bool>("anchorHasDownloadAttribute");
+    QTest::addColumn<QByteArray>("fileName");
+    QTest::addColumn<QByteArray>("fileContents");
+    QTest::addColumn<QByteArray>("fileMimeTypeDeclared");
+    QTest::addColumn<QByteArray>("fileMimeTypeDetected");
+    QTest::addColumn<QByteArray>("fileDisposition");
+    QTest::addColumn<bool>("fileHasReferer");
+    QTest::addColumn<DownloadTestFileAction>("fileAction");
+    QTest::addColumn<QWebEngineDownloadItem::DownloadType>("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 <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 = 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 90352310e0a2fb8443f3853d04f36faf53858144..441eea0fae8aff04bfc857bf71e5369d55908b0f 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