diff options
43 files changed, 1087 insertions, 430 deletions
diff --git a/configure.pri b/configure.pri index b592220a7..2d42613c0 100644 --- a/configure.pri +++ b/configure.pri @@ -467,10 +467,17 @@ defineTest(qtwebengine_isMacOsPlatformSupported) { defineTest(qtwebengine_isGCCVersionSupported) { # Keep in sync with src/webengine/doc/src/qtwebengine-platform-notes.qdoc - greaterThan(QMAKE_GCC_MAJOR_VERSION, 4):return(true) + lessThan(QMAKE_GCC_MAJOR_VERSION, 5) { + qtwebengine_platformError("requires at least gcc version 5, but using gcc version $${QMAKE_GCC_MAJOR_VERSION}.$${QMAKE_GCC_MINOR_VERSION}.") + return(false) + } - qtwebengine_platformError("requires at least gcc version 5, but using gcc version $${QMAKE_GCC_MAJOR_VERSION}.$${QMAKE_GCC_MINOR_VERSION}.") - return(false) + equals(QMAKE_GCC_MAJOR_VERSION, 5): equals(QMAKE_GCC_MINOR_VERSION, 4): equals(QMAKE_GCC_PATCH_VERSION, 0) { + qtwebengine_platformError("gcc version 5.4.0 is blacklisted due to internal compiler errors.") + return(false) + } + + return(true) } defineTest(qtwebengine_isBuildingOnWin32) { diff --git a/dist/changes-5.15.2 b/dist/changes-5.15.2 index de6ffc584..792b9afd6 100644 --- a/dist/changes-5.15.2 +++ b/dist/changes-5.15.2 @@ -39,7 +39,7 @@ Chromium -------- - The Chromium version has been updated to 83.0.4103.122 - - Security fixes from Chromium up to version 86.0.4240.111, including: + - Security fixes from Chromium up to version 86.0.4240.183, including: - CVE-2020-6540: Heap buffer overflow in Skia - CVE-2020-6557: Inappropriate implementation in networking - CVE-2020-6561: Inappropriate implementation in Content Security Policy @@ -75,8 +75,12 @@ Chromium - CVE-2020-16001: Use after free in media. - CVE-2020-16002: Use after free in PDFium - CVE-2020-16003: Use after free in printing + - CVE-2020-16005: Insufficient policy enforcement in ANGLE + - CVE-2020-16008: Stack buffer overflow in WebRTC + - CVE-2020-16009: Inappropriate implementation in V8 + - CVE-2020-16011: Heap buffer overflow in UI on Windows. - Security bug 1106091 - Security bug 1107824 - Security bug 1111149 - Security bug 1125199 - + - Security bug 1137608 diff --git a/src/3rdparty b/src/3rdparty -Subproject 138a7203f16cf356e9d4dac697920a22437014b +Subproject bb90182aa90ddc886c2cb41fa34bad1412ac6ed diff --git a/src/buildtools/configure.json b/src/buildtools/configure.json index 2da87a11c..24ffa71aa 100644 --- a/src/buildtools/configure.json +++ b/src/buildtools/configure.json @@ -75,9 +75,9 @@ ] }, "webengine-harfbuzz": { - "label": "harfbuzz >= 2.2.0", + "label": "harfbuzz >= 2.4.0", "sources": [ - { "type": "pkgConfig", "args": "harfbuzz >= 2.2.0" } + { "type": "pkgConfig", "args": "harfbuzz >= 2.4.0 harfbuzz-subset >= 2.4.0" } ] }, "webengine-jpeglib": { @@ -545,6 +545,11 @@ "condition": "config.unix && features.system-harfbuzz && libs.webengine-harfbuzz", "output": [ "privateFeature" ] }, + "webengine-qt-harfbuzz" : { + "label": "qtharfbuzz", + "condition": "config.static && !features.system-harfbuzz && features.harfbuzz", + "output": [ "privateFeature" ] + }, "webengine-system-glib" : { "label": "glib", "condition": "config.unix && libs.webengine-glib", @@ -560,6 +565,11 @@ "condition": "config.unix && features.system-zlib && libs.webengine-zlib", "output": [ "privateFeature" ] }, + "webengine-qt-zlib" : { + "label": "QtZlib", + "condition": "config.static && !features.system-zlib", + "output": [ "privateFeature" ] + }, "webengine-system-libevent" : { "label": "libevent", "condition": "config.unix && libs.webengine-libevent", @@ -580,11 +590,21 @@ "condition": "config.unix && features.system-png && libs.webengine-png", "output": [ "privateFeature" ] }, + "webengine-qt-png" : { + "label": "qtlibpng", + "condition" : "config.static && !features.system-png && features.png", + "output": [ "privateFeature" ] + }, "webengine-system-jpeg" : { "label": "JPEG", "condition": "config.unix && features.system-jpeg && libs.webengine-jpeglib", "output": [ "privateFeature" ] }, + "webengine-qt-jpeg" : { + "label": "qtlibjpeg", + "condition": "config.static && !features.system-jpeg && features.jpeg", + "output": [ "privateFeature" ] + }, "webengine-system-re2": { "label": "re2", "condition": "config.unix && libs.webengine-re2", @@ -627,6 +647,11 @@ "condition": "config.unix && features.system-freetype && libs.webengine-freetype", "output": [ "privateFeature" ] }, + "webengine-qt-freetype" : { + "label": "qtfreetype", + "condition": "config.static && !features.system-freetype && features.freetype", + "output": [ "privateFeature" ] + }, "webengine-system-libvpx" : { "label": "libvpx", "condition": "config.unix && libs.webengine-libvpx", @@ -787,6 +812,17 @@ "webengine-system-harfbuzz", "webengine-system-freetype" ] + }, + { + "section": "Qt 3rdparty libs", + "condition": "config.static", + "entries": [ + "webengine-qt-freetype", + "webengine-qt-harfbuzz", + "webengine-qt-png", + "webengine-qt-jpeg", + "webengine-qt-zlib" + ] } ] } diff --git a/src/core/api/qwebenginepage.cpp b/src/core/api/qwebenginepage.cpp index 5e029714d..aad929611 100644 --- a/src/core/api/qwebenginepage.cpp +++ b/src/core/api/qwebenginepage.cpp @@ -360,8 +360,13 @@ QWebEnginePagePrivate::adoptNewWindow(QSharedPointer<WebContentsAdapter> newWebC Q_UNUSED(targetUrl); QWebEnginePage *newPage = q->createWindow(toWindowType(disposition)); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) if (!newPage) return nullptr; +#else + if (!newPage) + return adapter; +#endif if (!newWebContents->webContents()) return newPage->d_func()->adapter; // Reuse existing adapter diff --git a/src/core/media_capture_devices_dispatcher.cpp b/src/core/media_capture_devices_dispatcher.cpp index 62d218625..9600f220e 100644 --- a/src/core/media_capture_devices_dispatcher.cpp +++ b/src/core/media_capture_devices_dispatcher.cpp @@ -71,6 +71,12 @@ #include <QtCore/qcoreapplication.h> +#if defined(WEBRTC_USE_X11) +#include <dlfcn.h> +#include <X11/extensions/Xrandr.h> +#include <X11/Xlib.h> +#endif + namespace QtWebEngineCore { using content::BrowserThread; @@ -117,25 +123,16 @@ void getDevicesForDesktopCapture(blink::MediaStreamDevices *devices, content::DesktopMediaID getDefaultScreenId() { - // While this function is executing another thread may also want to create a - // DesktopCapturer [1]. Unfortunately, creating a DesktopCapturer is not - // thread safe on X11 due to the use of webrtc::XErrorTrap. It's safe to - // disable this code on X11 since we don't actually need to create a - // DesktopCapturer to get the screen id anyway - // (ScreenCapturerLinux::GetSourceList always returns 0 as the id). - // - // [1]: webrtc::InProcessVideoCaptureDeviceLauncher::DoStartDesktopCaptureOnDeviceThread - -#if QT_CONFIG(webengine_webrtc) && !defined(WEBRTC_USE_X11) +#if QT_CONFIG(webengine_webrtc) // Source id patterns are different across platforms. - // On Linux, the hardcoded value "0" is used. + // On Linux and macOS, the source ids are randomish numbers assigned by the OS. // On Windows, the screens are enumerated consecutively in increasing order from 0. - // On macOS the source ids are randomish numbers assigned by the OS. // In order to provide a correct screen id, we query for the available screen ids, and // select the first one as the main display id. +#if !defined(WEBRTC_USE_X11) // The code is based on the file - // src/chrome/browser/extensions/api/desktop_capture/desktop_capture_base.cc. + // chrome/browser/media/webrtc/native_desktop_media_list.cc. webrtc::DesktopCaptureOptions options = webrtc::DesktopCaptureOptions::CreateDefault(); options.set_disable_effects(false); @@ -150,7 +147,60 @@ content::DesktopMediaID getDefaultScreenId() } } } -#endif +#else + // This is a workaround to avoid thread issues with DesktopCapturer [1]. Unfortunately, + // creating a DesktopCapturer is not thread safe on X11 due to the use of webrtc::XErrorTrap. + // Can be removed if https://crbug.com/2022 and/or https://crbug.com/570852 are fixed. + // The code is based on the file + // third_party/webrtc/modules/desktop_capture/linux/screen_capturer_x11.cc. + // + // [1]: webrtc::InProcessVideoCaptureDeviceLauncher::DoStartDesktopCaptureOnDeviceThread + Display *display = XOpenDisplay(nullptr); + if (!display) { + qWarning("Unable to open display."); + return content::DesktopMediaID(content::DesktopMediaID::TYPE_SCREEN, 0); + } + + int randrEventBase = 0; + int errorBaseIgnored = 0; + if (!XRRQueryExtension(display, &randrEventBase, &errorBaseIgnored)) { + qWarning("X server does not support XRandR."); + return content::DesktopMediaID(content::DesktopMediaID::TYPE_SCREEN, 0); + } + + int majorVersion = 0; + int minorVersion = 0; + if (!XRRQueryVersion(display, &majorVersion, &minorVersion)) { + qWarning("X server does not support XRandR."); + return content::DesktopMediaID(content::DesktopMediaID::TYPE_SCREEN, 0); + } + + if (majorVersion < 1 || (majorVersion == 1 && minorVersion < 5)) { + qWarning("XRandR entension is older than v1.5."); + return content::DesktopMediaID(content::DesktopMediaID::TYPE_SCREEN, 0); + } + + typedef XRRMonitorInfo *(*GetMonitorsFunc)(Display *, Window, Bool, int *); + GetMonitorsFunc getMonitors = reinterpret_cast<GetMonitorsFunc>(dlsym(RTLD_DEFAULT, "XRRGetMonitors")); + typedef void (*FreeMonitorsFunc)(XRRMonitorInfo*); + FreeMonitorsFunc freeMonitors = reinterpret_cast<FreeMonitorsFunc>(dlsym(RTLD_DEFAULT, "XRRFreeMonitors")); + if (!getMonitors && !freeMonitors) { + qWarning("Unable to link XRandR monitor functions."); + return content::DesktopMediaID(content::DesktopMediaID::TYPE_SCREEN, 0); + } + + Window rootWindow = RootWindow(display, DefaultScreen(display)); + if (rootWindow == BadValue) { + qWarning("Unable to get the root window."); + return content::DesktopMediaID(content::DesktopMediaID::TYPE_SCREEN, 0); + } + + int numMonitors = 0; + XRRMonitorInfo *monitors = getMonitors(display, rootWindow, true, &numMonitors); + if (numMonitors > 0) + return content::DesktopMediaID(content::DesktopMediaID::TYPE_SCREEN, monitors[0].name); +#endif // !defined(WEBRTC_USE_X11) +#endif // QT_CONFIG(webengine_webrtc) return content::DesktopMediaID(content::DesktopMediaID::TYPE_SCREEN, 0); } @@ -272,7 +322,8 @@ void MediaCaptureDevicesDispatcher::handleMediaAccessPermissionResponse(content: break; } } else if (desktopVideoRequested) { - getDevicesForDesktopCapture(&devices, getDefaultScreenId(), desktopAudioRequested, + bool captureAudio = desktopAudioRequested && m_loopbackAudioSupported; + getDevicesForDesktopCapture(&devices, getDefaultScreenId(), captureAudio, request.video_type, request.audio_type); } } @@ -309,6 +360,10 @@ MediaCaptureDevicesDispatcher::MediaCaptureDevicesDispatcher() // content::NOTIFICATION_WEB_CONTENTS_DESTROYED, and that will result in // possible use after free. DCHECK_CURRENTLY_ON(BrowserThread::UI); +#if defined(OS_WIN) + // Currently loopback audio capture is supported only on Windows. + m_loopbackAudioSupported = true; +#endif m_notificationsRegistrar.Add(this, content::NOTIFICATION_WEB_CONTENTS_DESTROYED, content::NotificationService::AllSources()); } @@ -383,9 +438,11 @@ void MediaCaptureDevicesDispatcher::processDesktopCaptureAccessRequest(content:: } // Audio is only supported for screen capture streams. - bool capture_audio = (mediaId.type == content::DesktopMediaID::TYPE_SCREEN && request.audio_type == MediaStreamType::GUM_DESKTOP_AUDIO_CAPTURE); + bool audioRequested = request.audio_type == MediaStreamType::GUM_DESKTOP_AUDIO_CAPTURE; + bool audioSupported = (mediaId.type == content::DesktopMediaID::TYPE_SCREEN && m_loopbackAudioSupported); + bool captureAudio = (audioRequested && audioSupported); - getDevicesForDesktopCapture(&devices, mediaId, capture_audio, request.video_type, request.audio_type); + getDevicesForDesktopCapture(&devices, mediaId, captureAudio, request.video_type, request.audio_type); if (devices.empty()) std::move(callback).Run(devices, MediaStreamRequestResult::INVALID_STATE, diff --git a/src/core/media_capture_devices_dispatcher.h b/src/core/media_capture_devices_dispatcher.h index 6a67a53e9..17cb5d5c9 100644 --- a/src/core/media_capture_devices_dispatcher.h +++ b/src/core/media_capture_devices_dispatcher.h @@ -127,6 +127,8 @@ private: content::NotificationRegistrar m_notificationsRegistrar; + bool m_loopbackAudioSupported = false; + DISALLOW_COPY_AND_ASSIGN(MediaCaptureDevicesDispatcher); }; diff --git a/src/core/net/proxying_url_loader_factory_qt.cpp b/src/core/net/proxying_url_loader_factory_qt.cpp index 235079c26..2b1472e88 100644 --- a/src/core/net/proxying_url_loader_factory_qt.cpp +++ b/src/core/net/proxying_url_loader_factory_qt.cpp @@ -271,9 +271,20 @@ void InterceptedRequest::ContinueAfterIntercept() DCHECK_CURRENTLY_ON(content::BrowserThread::UI); if (request_info_.changed()) { - if (request_info_.d_ptr->shouldBlockRequest) + QWebEngineUrlRequestInfoPrivate &info = *request_info_.d_ptr; + if (info.shouldBlockRequest) return SendErrorAndCompleteImmediately(net::ERR_BLOCKED_BY_CLIENT); - if (request_info_.d_ptr->shouldRedirectRequest) { + + for (auto header = info.extraHeaders.constBegin(); header != info.extraHeaders.constEnd(); ++header) { + std::string h = header.key().toStdString(); + if (base::LowerCaseEqualsASCII(h, "referer")) { + request_.referrer = GURL(header.value().toStdString()); + } else { + request_.headers.SetHeader(h, header.value().toStdString()); + } + } + + if (info.shouldRedirectRequest) { net::URLRequest::FirstPartyURLPolicy first_party_url_policy = request_.update_first_party_url_on_redirect ? net::URLRequest::UPDATE_FIRST_PARTY_URL_ON_REDIRECT : net::URLRequest::NEVER_CHANGE_FIRST_PARTY_URL; @@ -295,18 +306,6 @@ void InterceptedRequest::ContinueAfterIntercept() target_client_->OnReceiveRedirect(redirectInfo, std::move(current_response_)); return; } - - if (!request_info_.d_ptr->extraHeaders.isEmpty()) { - auto end = request_info_.d_ptr->extraHeaders.constEnd(); - for (auto header = request_info_.d_ptr->extraHeaders.constBegin(); header != end; ++header) { - std::string h = header.key().toStdString(); - if (base::LowerCaseEqualsASCII(h, "referer")) { - request_.referrer = GURL(header.value().toStdString()); - } else { - request_.headers.SetHeader(h, header.value().toStdString()); - } - } - } } if (!target_loader_ && target_factory_) { diff --git a/src/core/ozone/gl_ozone_egl_qt.cpp b/src/core/ozone/gl_ozone_egl_qt.cpp index a8b7cdfe4..14ba5e8d9 100644 --- a/src/core/ozone/gl_ozone_egl_qt.cpp +++ b/src/core/ozone/gl_ozone_egl_qt.cpp @@ -79,8 +79,8 @@ bool GLOzoneEGLQt::LoadGLES2Bindings(gl::GLImplementation /*implementation*/) "eglGetProcAddress")); if (!get_proc_address) { // QTBUG-63341 most likely libgles2 not linked with libegl -> fallback to qpa - QFunctionPointer address = GLContextHelper::getEglGetProcAddress(); - get_proc_address = reinterpret_cast<gl::GLGetProcAddressProc>(address); + get_proc_address = + reinterpret_cast<gl::GLGetProcAddressProc>(GLContextHelper::getEglGetProcAddress()); } if (!get_proc_address) { diff --git a/src/core/render_widget_host_view_qt.cpp b/src/core/render_widget_host_view_qt.cpp index fbb4c5bd6..05ac76350 100644 --- a/src/core/render_widget_host_view_qt.cpp +++ b/src/core/render_widget_host_view_qt.cpp @@ -63,7 +63,9 @@ #include "content/browser/renderer_host/render_view_host_delegate.h" #include "content/browser/renderer_host/render_view_host_impl.h" #include "content/browser/renderer_host/render_widget_host_input_event_router.h" +#include "content/browser/renderer_host/ui_events_helper.h" #include "content/common/content_switches_internal.h" +#include "content/browser/renderer_host/ui_events_helper.h" #include "content/common/cursors/webcursor.h" #include "content/common/input_messages.h" #include "third_party/skia/include/core/SkColor.h" @@ -86,6 +88,7 @@ #include <QGuiApplication> #include <QPixmap> +#include <QScopeGuard> #include <QScreen> #include <QWindow> @@ -767,7 +770,8 @@ void RenderWidgetHostViewQt::notifyHidden() void RenderWidgetHostViewQt::ProcessAckedTouchEvent(const content::TouchEventWithLatencyInfo &touch, content::InputEventAckState ack_result) { Q_UNUSED(touch); const bool eventConsumed = ack_result == content::INPUT_EVENT_ACK_STATE_CONSUMED; - m_gestureProvider.OnTouchEventAck(touch.event.unique_touch_event_id, eventConsumed, /*fixme: ?? */false); + const bool isSetNonBlocking = content::InputEventAckStateIsSetNonBlocking(ack_result); + m_gestureProvider.OnTouchEventAck(touch.event.unique_touch_event_id, eventConsumed, isSetNonBlocking); } void RenderWidgetHostViewQt::processMotionEvent(const ui::MotionEvent &motionEvent) diff --git a/src/core/render_widget_host_view_qt_delegate_client.cpp b/src/core/render_widget_host_view_qt_delegate_client.cpp index 1a41e6850..f4933d560 100644 --- a/src/core/render_widget_host_view_qt_delegate_client.cpp +++ b/src/core/render_widget_host_view_qt_delegate_client.cpp @@ -51,6 +51,7 @@ #include <QEvent> #include <QInputMethodEvent> +#include <QScopeGuard> #include <QSGNode> #include <QStyleHints> #include <QTextFormat> @@ -72,40 +73,38 @@ static inline int firstAvailableId(const QMap<int, int> &map) return usedIds.first_unmarked_bit(); } -static QList<QTouchEvent::TouchPoint> -mapTouchPointIds(const QList<QTouchEvent::TouchPoint> &inputPoints) +typedef QPair<int, QTouchEvent::TouchPoint> TouchPoint; +QList<TouchPoint> RenderWidgetHostViewQtDelegateClient::mapTouchPointIds(const QList<QTouchEvent::TouchPoint> &input) { - static QMap<int, int> touchIdMapping; - QList<QTouchEvent::TouchPoint> outputPoints = inputPoints; - for (int i = 0; i < outputPoints.size(); ++i) { - QTouchEvent::TouchPoint &point = outputPoints[i]; + QList<TouchPoint> output; + for (int i = 0; i < input.size(); ++i) { + const QTouchEvent::TouchPoint &point = input[i]; int qtId = point.id(); - QMap<int, int>::const_iterator it = touchIdMapping.find(qtId); - if (it == touchIdMapping.end()) - it = touchIdMapping.insert(qtId, firstAvailableId(touchIdMapping)); - QMutableEventPoint &mut = QMutableEventPoint::from(point); - mut.setId(it.value()); + QMap<int, int>::const_iterator it = m_touchIdMapping.find(qtId); + if (it == m_touchIdMapping.end()) { + Q_ASSERT_X(m_touchIdMapping.size() <= 16, "", "Number of mapped ids can't exceed 16 for velocity tracker"); + it = m_touchIdMapping.insert(qtId, firstAvailableId(m_touchIdMapping)); + } - if (point.state() == QEventPoint::State::Released) - touchIdMapping.remove(qtId); + output.append(qMakePair(it.value(), point)); } - return outputPoints; -} + Q_ASSERT(output.size() == std::accumulate(output.cbegin(), output.cend(), QSet<int>(), + [] (QSet<int> s, const TouchPoint &p) { s.insert(p.second.id()); return s; }).size()); -static inline bool compareTouchPoints(const QTouchEvent::TouchPoint &lhs, - const QTouchEvent::TouchPoint &rhs) -{ - // TouchPointPressed < TouchPointMoved < TouchPointReleased - return lhs.state() < rhs.state(); + for (auto &&point : qAsConst(input)) + if (point.state() == Qt::TouchPointReleased) + m_touchIdMapping.remove(point.id()); + + return output; } static uint32_t s_eventId = 0; class MotionEventQt : public ui::MotionEvent { public: - MotionEventQt(const QList<QTouchEvent::TouchPoint> &touchPoints, + MotionEventQt(const QList<TouchPoint> &touchPoints, const base::TimeTicks &eventTime, Action action, const Qt::KeyboardModifiers modifiers, int index = -1) : touchPoints(touchPoints) @@ -115,8 +114,12 @@ public: , flags(flagsFromModifiers(modifiers)) , index(index) { - // ACTION_DOWN and ACTION_UP must be accesssed through pointer_index 0 - Q_ASSERT((action != Action::DOWN && action != Action::UP) || index == 0); + // index is only valid for ACTION_DOWN and ACTION_UP and should correspond to the point causing it + // see blink_event_util.cc:ToWebTouchPointState for details + Q_ASSERT_X((action != Action::POINTER_DOWN && action != Action::POINTER_UP && index == -1) + || (action == Action::POINTER_DOWN && index >= 0 && touchPoint(index).state() == Qt::TouchPointPressed) + || (action == Action::POINTER_UP && index >= 0 && touchPoint(index).state() == Qt::TouchPointReleased), + "MotionEventQt", qPrintable(QString("action: %1, index: %2, state: %3").arg(int(action)).arg(index).arg(touchPoint(index).state()))); } uint32_t GetUniqueEventId() const override { return eventId; } @@ -125,39 +128,39 @@ public: size_t GetPointerCount() const override { return touchPoints.size(); } int GetPointerId(size_t pointer_index) const override { - return touchPoints.at(pointer_index).id(); + return touchPoints[pointer_index].first; } float GetX(size_t pointer_index) const override { - return touchPoints.at(pointer_index).position().x(); + return touchPoint(pointer_index).position().x(); } float GetY(size_t pointer_index) const override { - return touchPoints.at(pointer_index).position().y(); + return touchPoint(pointer_index).position().y(); } float GetRawX(size_t pointer_index) const override { - return touchPoints.at(pointer_index).globalPosition().x(); + return touchPoint(pointer_index).globalPosition().x(); } float GetRawY(size_t pointer_index) const override { - return touchPoints.at(pointer_index).globalPosition().y(); + return touchPoint(pointer_index).globalPosition().y(); } float GetTouchMajor(size_t pointer_index) const override { - QSizeF diams = touchPoints.at(pointer_index).ellipseDiameters(); + QSizeF diams = touchPoint(pointer_index).ellipseDiameters(); return std::max(diams.height(), diams.width()); } float GetTouchMinor(size_t pointer_index) const override { - QSizeF diams = touchPoints.at(pointer_index).ellipseDiameters(); + QSizeF diams = touchPoint(pointer_index).ellipseDiameters(); return std::min(diams.height(), diams.width()); } float GetOrientation(size_t pointer_index) const override { return 0; } int GetFlags() const override { return flags; } float GetPressure(size_t pointer_index) const override { - return touchPoints.at(pointer_index).pressure(); + return touchPoint(pointer_index).pressure(); } float GetTiltX(size_t pointer_index) const override { return 0; } float GetTiltY(size_t pointer_index) const override { return 0; } @@ -184,12 +187,13 @@ public: int GetButtonState() const override { return 0; } private: - QList<QTouchEvent::TouchPoint> touchPoints; + QList<TouchPoint> touchPoints; base::TimeTicks eventTime; Action action; const uint32_t eventId; int flags; int index; + const QTouchEvent::TouchPoint& touchPoint(size_t i) const { return touchPoints[i].second; } }; RenderWidgetHostViewQtDelegateClient::RenderWidgetHostViewQtDelegateClient( @@ -567,20 +571,39 @@ void RenderWidgetHostViewQtDelegateClient::handleTouchEvent(QTouchEvent *event) // Calculate a delta between event timestamps and Now() on the first received event, and // apply this delta to all successive events. This delta is most likely smaller than it // should by calculating it here but this will hopefully cause less than one frame of delay. - base::TimeTicks eventTimestamp = - base::TimeTicks() + base::TimeDelta::FromMilliseconds(event->timestamp()); - static base::TimeDelta eventsToNowDelta = base::TimeTicks::Now() - eventTimestamp; - eventTimestamp += eventsToNowDelta; + base::TimeTicks eventTimestamp = base::TimeTicks() + base::TimeDelta::FromMilliseconds(event->timestamp()); + if (m_eventsToNowDelta == 0) + m_eventsToNowDelta = (base::TimeTicks::Now() - eventTimestamp).InMicroseconds(); + eventTimestamp += base::TimeDelta::FromMicroseconds(m_eventsToNowDelta); + + auto touchPoints = mapTouchPointIds(event->touchPoints()); + // Make sure that POINTER_DOWN action is delivered before MOVE, and MOVE before POINTER_UP + std::sort(touchPoints.begin(), touchPoints.end(), [] (const TouchPoint &l, const TouchPoint &r) { + return l.second.state() < r.second.state(); + }); + + auto sc = qScopeGuard([&] () { + switch (event->type()) { + case QEvent::TouchCancel: + for (auto &&it : qAsConst(touchPoints)) + m_touchIdMapping.remove(it.second.id()); + Q_FALLTHROUGH(); + + case QEvent::TouchEnd: + m_previousTouchPoints.clear(); + m_touchMotionStarted = false; + break; - QList<QTouchEvent::TouchPoint> touchPoints = mapTouchPointIds(event->touchPoints()); - // Make sure that ACTION_POINTER_DOWN is delivered before ACTION_MOVE, - // and ACTION_MOVE before ACTION_POINTER_UP. - std::sort(touchPoints.begin(), touchPoints.end(), compareTouchPoints); + default: + m_previousTouchPoints = touchPoints; + break; + } + }); + ui::MotionEvent::Action action; // Check first if the touch event should be routed to the selectionController if (!touchPoints.isEmpty()) { - ui::MotionEvent::Action action; - switch (touchPoints[0].state()) { + switch (touchPoints[0].second.state()) { case Qt::TouchPointPressed: action = ui::MotionEvent::Action::DOWN; break; @@ -594,32 +617,24 @@ void RenderWidgetHostViewQtDelegateClient::handleTouchEvent(QTouchEvent *event) action = ui::MotionEvent::Action::NONE; break; } - - MotionEventQt motionEvent(touchPoints, eventTimestamp, action, event->modifiers(), 0); - if (m_rwhv->getTouchSelectionController()->WillHandleTouchEvent(motionEvent)) { - m_previousTouchPoints = touchPoints; - event->accept(); - return; - } } else { // An empty touchPoints always corresponds to a TouchCancel event. // We can't forward touch cancellations without a previously processed touch event, // as Chromium expects the previous touchPoints for Action::CANCEL. // If both are empty that means the TouchCancel was sent without an ongoing touch, // so there's nothing to cancel anyway. + Q_ASSERT(event->type() == QEvent::TouchCancel); touchPoints = m_previousTouchPoints; if (touchPoints.isEmpty()) return; - MotionEventQt cancelEvent(touchPoints, eventTimestamp, ui::MotionEvent::Action::CANCEL, - event->modifiers()); - if (m_rwhv->getTouchSelectionController()->WillHandleTouchEvent(cancelEvent)) { - m_previousTouchPoints.clear(); - event->accept(); - return; - } + action = ui::MotionEvent::Action::CANCEL; } + MotionEventQt me(touchPoints, eventTimestamp, action, event->modifiers()); + if (m_rwhv->getTouchSelectionController()->WillHandleTouchEvent(me)) + return; + switch (event->type()) { case QEvent::TouchBegin: m_sendMotionActionDown = true; @@ -629,19 +644,17 @@ void RenderWidgetHostViewQtDelegateClient::handleTouchEvent(QTouchEvent *event) case QEvent::TouchUpdate: m_touchMotionStarted = true; break; - case QEvent::TouchCancel: { + case QEvent::TouchCancel: + { // Only process TouchCancel events received following a TouchBegin or TouchUpdate event if (m_touchMotionStarted) { - MotionEventQt cancelEvent(touchPoints, eventTimestamp, ui::MotionEvent::Action::CANCEL, - event->modifiers()); + MotionEventQt cancelEvent(touchPoints, eventTimestamp, ui::MotionEvent::Action::CANCEL, event->modifiers()); m_rwhv->processMotionEvent(cancelEvent); } - clearPreviousTouchMotionState(); return; } case QEvent::TouchEnd: - clearPreviousTouchMotionState(); m_rwhv->getTouchSelectionControllerClient()->onTouchUp(); break; default: @@ -661,34 +674,50 @@ void RenderWidgetHostViewQtDelegateClient::handleTouchEvent(QTouchEvent *event) #endif } - for (int i = 0; i < touchPoints.size(); ++i) { - ui::MotionEvent::Action action; - switch (touchPoints[i].state()) { - case Qt::TouchPointPressed: - if (m_sendMotionActionDown) { - action = ui::MotionEvent::Action::DOWN; - m_sendMotionActionDown = false; - } else { - action = ui::MotionEvent::Action::POINTER_DOWN; + // MEMO for the basis of this logic look into: + // * blink_event_util.cc:ToWebTouchPointState: which is used later to forward touch event + // composed from motion event after gesture recognition + // * gesture_detector.cc:GestureDetector::OnTouchEvent: contains logic for every motion + // event action and corresponding gesture recognition routines + // * input_router_imp.cc:InputRouterImp::SetMovementXYForTouchPoints: expectation about + // touch event content like number of points for different states + + int lastPressIndex = -1; + while ((lastPressIndex + 1) < touchPoints.size() && touchPoints[lastPressIndex + 1].second.state() == Qt::TouchPointPressed) + ++lastPressIndex; + + switch (event->type()) { + case QEvent::TouchBegin: + m_rwhv->processMotionEvent(MotionEventQt(touchPoints.mid(lastPressIndex), + eventTimestamp, ui::MotionEvent::Action::DOWN, event->modifiers())); + --lastPressIndex; + Q_FALLTHROUGH(); + + case QEvent::TouchUpdate: + for (; lastPressIndex >= 0; --lastPressIndex) { + Q_ASSERT(touchPoints[lastPressIndex].second.state() == Qt::TouchPointPressed); + MotionEventQt me(touchPoints.mid(lastPressIndex), eventTimestamp, ui::MotionEvent::Action::POINTER_DOWN, event->modifiers(), 0); + m_rwhv->processMotionEvent(me); + } + + if (event->touchPointStates() & Qt::TouchPointMoved) + m_rwhv->processMotionEvent(MotionEventQt(touchPoints, eventTimestamp, ui::MotionEvent::Action::MOVE, event->modifiers())); + + Q_FALLTHROUGH(); + + case QEvent::TouchEnd: + while (!touchPoints.isEmpty() && touchPoints.back().second.state() == Qt::TouchPointReleased) { + auto action = touchPoints.size() > 1 ? ui::MotionEvent::Action::POINTER_UP : ui::MotionEvent::Action::UP; + int index = action == ui::MotionEvent::Action::POINTER_UP ? touchPoints.size() - 1 : -1; + m_rwhv->processMotionEvent(MotionEventQt(touchPoints, eventTimestamp, action, event->modifiers(), index)); + touchPoints.pop_back(); } break; - case Qt::TouchPointMoved: - action = ui::MotionEvent::Action::MOVE; - break; - case Qt::TouchPointReleased: - action = touchPoints.size() > 1 ? ui::MotionEvent::Action::POINTER_UP - : ui::MotionEvent::Action::UP; - break; - default: - // Ignore Qt::TouchPointStationary - continue; - } - MotionEventQt motionEvent(touchPoints, eventTimestamp, action, event->modifiers(), i); - m_rwhv->processMotionEvent(motionEvent); + default: + Q_ASSERT_X(false, __FUNCTION__, "Other event types are expected to be already handled."); + break; } - - m_previousTouchPoints = touchPoints; } #if QT_CONFIG(tabletevent) diff --git a/src/core/render_widget_host_view_qt_delegate_client.h b/src/core/render_widget_host_view_qt_delegate_client.h index c338df95e..4ba5da227 100644 --- a/src/core/render_widget_host_view_qt_delegate_client.h +++ b/src/core/render_widget_host_view_qt_delegate_client.h @@ -140,9 +140,13 @@ private: std::string m_editCommand; // Touch - QList<QTouchEvent::TouchPoint> m_previousTouchPoints; + typedef QPair<int, QTouchEvent::TouchPoint> TouchPoint; + QList<TouchPoint> mapTouchPointIds(const QList<QTouchEvent::TouchPoint> &input); + QMap<int, int> m_touchIdMapping; + QList<TouchPoint> m_previousTouchPoints; bool m_touchMotionStarted = false; bool m_sendMotionActionDown = false; + int64_t m_eventsToNowDelta = 0; // delta for first touch in microseconds // IME bool m_receivedEmptyImeEvent = false; diff --git a/src/core/web_contents_delegate_qt.cpp b/src/core/web_contents_delegate_qt.cpp index d47d7dfab..2a0484234 100644 --- a/src/core/web_contents_delegate_qt.cpp +++ b/src/core/web_contents_delegate_qt.cpp @@ -106,7 +106,6 @@ WebContentsDelegateQt::WebContentsDelegateQt(content::WebContents *webContents, : m_viewClient(adapterClient) , m_faviconManager(new FaviconManager(webContents, adapterClient)) , m_findTextHelper(new FindTextHelper(webContents, adapterClient)) - , m_lastLoadProgress(-1) , m_loadingState(determineLoadingState(webContents)) , m_didStartLoadingSeen(m_loadingState == LoadingState::Loading) , m_frameFocusedObserver(adapterClient) @@ -128,7 +127,7 @@ content::WebContents *WebContentsDelegateQt::OpenURLFromTab(content::WebContents content::SiteInstance *target_site_instance = params.source_site_instance.get(); content::Referrer referrer = params.referrer; if (params.disposition != WindowOpenDisposition::CURRENT_TAB) { - QSharedPointer<WebContentsAdapter> targetAdapter = createWindow(0, params.disposition, gfx::Rect(), params.user_gesture); + QSharedPointer<WebContentsAdapter> targetAdapter = createWindow(nullptr, params.disposition, gfx::Rect(), toQt(params.url), params.user_gesture); if (targetAdapter) { if (targetAdapter->profile() != source->GetBrowserContext()) { target_site_instance = nullptr; @@ -137,6 +136,8 @@ content::WebContents *WebContentsDelegateQt::OpenURLFromTab(content::WebContents if (!targetAdapter->isInitialized()) targetAdapter->initialize(target_site_instance); target = targetAdapter->webContents(); + } else { + return target; } } Q_ASSERT(target); @@ -234,8 +235,8 @@ QUrl WebContentsDelegateQt::url(content::WebContents* source) const { } void WebContentsDelegateQt::AddNewContents(content::WebContents* source, std::unique_ptr<content::WebContents> new_contents, WindowOpenDisposition disposition, const gfx::Rect& initial_pos, bool user_gesture, bool* was_blocked) { - Q_UNUSED(source); - QSharedPointer<WebContentsAdapter> newAdapter = createWindow(std::move(new_contents), disposition, initial_pos, user_gesture); + Q_UNUSED(source) + QSharedPointer<WebContentsAdapter> newAdapter = createWindow(std::move(new_contents), disposition, initial_pos, m_initialTargetUrl, user_gesture); // Chromium can forget to pass user-agent override settings to new windows (see QTBUG-61774 and QTBUG-76249), // so set it here. Note the actual value doesn't really matter here. Only the second value does, but we try // to give the correct user-agent anyway. @@ -258,14 +259,14 @@ void WebContentsDelegateQt::CloseContents(content::WebContents *source) void WebContentsDelegateQt::LoadProgressChanged(double progress) { - if (!m_loadingErrorFrameList.isEmpty()) - return; - if (m_lastLoadProgress < 0) // suppress signals that aren't between loadStarted and loadFinished + QUrl current_url(m_viewClient->webContentsAdapter()->getNavigationEntryOriginalUrl(m_viewClient->webContentsAdapter()->currentNavigationEntryIndex())); + int p = qMin(qRound(progress * 100), 100); + + if (!m_loadingErrorFrameList.isEmpty() || !m_loadProgressMap.contains(current_url) || m_loadProgressMap[current_url] == 100 || p == 100) return; - int p = qMin(qRound(progress * 100), 100); - if (p > m_lastLoadProgress) { // ensure strict monotonic increase - m_lastLoadProgress = p; + if (p > m_loadProgressMap[current_url]) { // ensure strict monotonic increase + m_loadProgressMap[current_url] = p; m_viewClient->loadProgressChanged(p); } } @@ -341,16 +342,37 @@ void WebContentsDelegateQt::RenderViewHostChanged(content::RenderViewHost *, con void WebContentsDelegateQt::EmitLoadStarted(const QUrl &url, bool isErrorPage) { - if (m_lastLoadProgress >= 0 && m_lastLoadProgress < 100) // already running - return; for (auto &&wc : m_certificateErrorControllers) if (auto controller = wc.lock()) controller->deactivate(); m_certificateErrorControllers.clear(); + m_viewClient->loadStarted(url, isErrorPage); m_viewClient->updateNavigationActions(); + + if ((url.hasFragment() || m_lastLoadedUrl.hasFragment()) + && url.adjusted(QUrl::RemoveFragment) == m_lastLoadedUrl.adjusted(QUrl::RemoveFragment) + && !m_isNavigationCommitted) { + m_loadProgressMap.insert(url, 100); + m_lastLoadedUrl = url; + m_viewClient->loadProgressChanged(100); + return; + } + + if (!m_loadProgressMap.isEmpty()) { + QMap<QUrl, int>::iterator it = m_loadProgressMap.begin(); + while (it != m_loadProgressMap.end()) { + if (it.value() == 100) { + it = m_loadProgressMap.erase(it); + continue; + } + ++it; + } + } + + m_lastLoadedUrl = url; + m_loadProgressMap.insert(url, 0); m_viewClient->loadProgressChanged(0); - m_lastLoadProgress = 0; } void WebContentsDelegateQt::DidStartNavigation(content::NavigationHandle *navigation_handle) @@ -368,11 +390,15 @@ void WebContentsDelegateQt::DidStartNavigation(content::NavigationHandle *naviga void WebContentsDelegateQt::EmitLoadFinished(bool success, const QUrl &url, bool isErrorPage, int errorCode, const QString &errorDescription) { - if (m_lastLoadProgress < 0) // not currently running + // When error page enabled we don't need to send the error page load finished signal + if (m_loadProgressMap[url] == 100) return; - if (m_lastLoadProgress < 100) - m_viewClient->loadProgressChanged(100); - m_lastLoadProgress = -1; + + m_lastLoadedUrl = url; + m_loadProgressMap[url] = 100; + m_isNavigationCommitted = false; + m_viewClient->loadProgressChanged(100); + m_viewClient->loadFinished(success, url, isErrorPage, errorCode, errorDescription); m_viewClient->updateNavigationActions(); } @@ -397,8 +423,10 @@ void WebContentsDelegateQt::DidFinishNavigation(content::NavigationHandle *navig profileAdapter->visitedLinksManager()->addUrl(url); } + m_isNavigationCommitted = true; EmitLoadCommitted(); } + // Success is reported by DidFinishLoad, but DidFailLoad is now dead code and needs to be handled below if (navigation_handle->GetNetErrorCode() == net::OK) return; @@ -680,7 +708,7 @@ void WebContentsDelegateQt::overrideWebPreferences(content::WebContents *webCont QSharedPointer<WebContentsAdapter> WebContentsDelegateQt::createWindow(std::unique_ptr<content::WebContents> new_contents, - WindowOpenDisposition disposition, const gfx::Rect &initial_pos, + WindowOpenDisposition disposition, const gfx::Rect &initial_pos, const QUrl &url, bool user_gesture) { QSharedPointer<WebContentsAdapter> newAdapter = QSharedPointer<WebContentsAdapter>::create(std::move(new_contents)); @@ -688,7 +716,7 @@ WebContentsDelegateQt::createWindow(std::unique_ptr<content::WebContents> new_co return m_viewClient->adoptNewWindow( std::move(newAdapter), static_cast<WebContentsAdapterClient::WindowOpenDisposition>(disposition), user_gesture, - toQt(initial_pos), m_initialTargetUrl); + toQt(initial_pos), url); } void WebContentsDelegateQt::allowCertificateError( diff --git a/src/core/web_contents_delegate_qt.h b/src/core/web_contents_delegate_qt.h index 4677a780a..1481c7904 100644 --- a/src/core/web_contents_delegate_qt.h +++ b/src/core/web_contents_delegate_qt.h @@ -202,6 +202,7 @@ private: QSharedPointer<WebContentsAdapter> createWindow(std::unique_ptr<content::WebContents> new_contents, WindowOpenDisposition disposition, const gfx::Rect &initial_pos, + const QUrl &url, bool user_gesture); void EmitLoadStarted(const QUrl &url, bool isErrorPage = false); void EmitLoadFinished(bool success, const QUrl &url, bool isErrorPage = false, int errorCode = 0, const QString &errorDescription = QString()); @@ -219,7 +220,6 @@ private: SavePageInfo m_savePageInfo; QSharedPointer<FilePickerController> m_filePickerController; QUrl m_initialTargetUrl; - int m_lastLoadProgress; LoadingState m_loadingState; bool m_didStartLoadingSeen; FrameFocusedObserver m_frameFocusedObserver; @@ -231,6 +231,9 @@ private: int m_desktopStreamCount = 0; mutable bool m_pendingUrlUpdate = false; + QMap<QUrl, int> m_loadProgressMap; + QUrl m_lastLoadedUrl; + bool m_isNavigationCommitted = false; base::WeakPtrFactory<WebContentsDelegateQt> m_weakPtrFactory { this }; QList<QWeakPointer<CertificateErrorController>> m_certificateErrorControllers; }; diff --git a/src/core/web_event_factory.cpp b/src/core/web_event_factory.cpp index e77463930..ca2f13a06 100644 --- a/src/core/web_event_factory.cpp +++ b/src/core/web_event_factory.cpp @@ -1280,6 +1280,29 @@ static unsigned mouseButtonsModifiersForEvent(const T* event) return ret; } +static WebInputEvent::Modifiers lockKeyModifiers(const quint32 nativeModifiers) +{ + unsigned result = 0; + if (keyboardDriver() == KeyboardDriver::Xkb) { + if (nativeModifiers & 0x42) /* Caps_Lock */ + result |= WebInputEvent::kCapsLockOn; + if (nativeModifiers & 0x4d) /* Num_Lock */ + result |= WebInputEvent::kNumLockOn; + } else if (keyboardDriver() == KeyboardDriver::Windows) { + if (nativeModifiers & 0x100) /* CapsLock */ + result |= WebInputEvent::kCapsLockOn; + if (nativeModifiers & 0x200) /* NumLock */ + result |= WebInputEvent::kNumLockOn; + if (nativeModifiers & 0x400) /* ScrollLock */ + result |= WebInputEvent::kScrollLockOn; + } else if (keyboardDriver() == KeyboardDriver::Cocoa) { + if (nativeModifiers & 0x10000) /* NSEventModifierFlagCapsLock */ + result |= WebInputEvent::kCapsLockOn; + } + + return static_cast<WebInputEvent::Modifiers>(result); +} + // If only a modifier key is pressed, Qt only reports the key code. // But Chromium also expects the modifier being set. static inline WebInputEvent::Modifiers modifierForKeyCode(int key) @@ -1328,13 +1351,14 @@ static inline WebInputEvent::Modifiers modifiersForEvent(const QInputEvent* even if (keyEvent->isAutoRepeat()) result |= WebInputEvent::kIsAutoRepeat; result |= modifierForKeyCode(qtKeyForKeyEvent(keyEvent)); + result |= lockKeyModifiers(keyEvent->nativeModifiers()); break; } default: break; } - return (WebInputEvent::Modifiers)result; + return static_cast<WebInputEvent::Modifiers>(result); } static inline Qt::KeyboardModifiers keyboardModifiersForModifier(unsigned int modifier) diff --git a/src/pdf/config/common.pri b/src/pdf/config/common.pri index dd5bfa293..303c4a91c 100644 --- a/src/pdf/config/common.pri +++ b/src/pdf/config/common.pri @@ -1,6 +1,35 @@ include($$QTWEBENGINE_OUT_ROOT/src/pdf/qtpdf-config.pri) QT_FOR_CONFIG += pdf-private +qtConfig(webengine-qt-png) { + gn_args += pdfium_use_qt_libpng=true + gn_args += "pdfium_qt_libpng_includes=\"$$system_path($$QMAKE_INCDIR_LIBPNG)\"" +} + +#qtConfig(webengine-qt-jpeg) { +# gn_args += use_qt_libjpeg=true +# gn_args += "qt_libjpeg_includes=\"$$system_path($$QMAKE_INCDIR_LIBJPEG)\"" +#} + +qtConfig(webengine-qt-harfbuzz) { + gn_args += use_qt_harfbuzz=true + gn_args += "qt_harfbuzz_includes=\"$$system_path($$QMAKE_INCDIR_HARFBUZZ)\"" +} + +qtConfig(webengine-qt-freetype) { + gn_args += use_qt_freetype=true + gn_args += "qt_freetype_includes=\"$$system_path($$QMAKE_INCDIR_FREETYPE)\"" +} + +qtConfig(webengine-qt-zlib) { + gn_args += use_qt_zlib = true + gn_args += "qt_zlib_includes=\["\ + "\"$$system_path($$[QT_INSTALL_HEADERS])\"," \ + "\"$$system_path($$[QT_INSTALL_HEADERS]/QtCore)\"," \ + "\"$$system_path($$[QT_INSTALL_HEADERS]/QtZlib)\"\]" + gn_args += "qt_zlib=\"$$system_path($$[QT_INSTALL_LIBS]/libQt5Core.a)\"" +} + qtConfig(pdf-v8) { gn_args += pdf_enable_v8=true } else { diff --git a/src/pdf/config/ios.pri b/src/pdf/config/ios.pri index 1dcbeffde..cd7597d85 100644 --- a/src/pdf/config/ios.pri +++ b/src/pdf/config/ios.pri @@ -37,6 +37,7 @@ clang_base_path=\"$${clang_dir}\" \ ios_enable_code_signing=false \ target_os=\"ios\" \ ios_deployment_target=\"$${QMAKE_IOS_DEPLOYMENT_TARGET}\" \ +mac_sdk_min=\"$${QMAKE_MAC_SDK_VERSION_MAJOR_MINOR}\" \ enable_ios_bitcode=true \ use_jumbo_build=false diff --git a/src/pdf/configure.json b/src/pdf/configure.json index ddc0e99dc..069893660 100644 --- a/src/pdf/configure.json +++ b/src/pdf/configure.json @@ -2,7 +2,7 @@ "module": "pdf", "depends" : [ "buildtools-private" ], "testDir": "../../config.tests", - "condition": "features.build-qtpdf && features.webengine-qtpdf-support", + "condition": "module.gui && features.webengine-qtpdf-support && features.build-qtpdf", "libraries": { }, "tests": { diff --git a/src/pdf/pdfcore.pro b/src/pdf/pdfcore.pro index 2dfe39dc0..998cddb7d 100644 --- a/src/pdf/pdfcore.pro +++ b/src/pdf/pdfcore.pro @@ -77,4 +77,11 @@ HEADERS += \ api/qpdfselection.h \ api/qpdfselection_p.h \ + +qtConfig(webengine-qt-freetype): QMAKE_USE_PRIVATE+= freetype +qtConfig(webengine-qt-png): QMAKE_USE_PRIVATE+= libpng +qtConfig(webengine-qt-harfbuzz): QMAKE_USE_PRIVATE+= harfbuzz +#qtConfig(webengine-qt-jpeg): QMAKE_USE_PRIVATE+= libjpeg +qtConfig(webengine-qt-zlib){} #qtzlib is a part of QtCore + load(qt_module) diff --git a/tests/auto/core/qwebengineurlrequestinterceptor/resources/content.html b/tests/auto/core/qwebengineurlrequestinterceptor/resources/content.html index 360ad65ef..84bf55036 100644 --- a/tests/auto/core/qwebengineurlrequestinterceptor/resources/content.html +++ b/tests/auto/core/qwebengineurlrequestinterceptor/resources/content.html @@ -1,5 +1,6 @@ <html> +<head><link rel="icon" href="data:,"></head> <body> -<a>This is test content</a> +<a>Simple test page without favicon (meaning no separate request from http server)</a> </body> </html> diff --git a/tests/auto/core/qwebengineurlrequestinterceptor/resources/content2.html b/tests/auto/core/qwebengineurlrequestinterceptor/resources/content2.html new file mode 100644 index 000000000..84bf55036 --- /dev/null +++ b/tests/auto/core/qwebengineurlrequestinterceptor/resources/content2.html @@ -0,0 +1,6 @@ +<html> +<head><link rel="icon" href="data:,"></head> +<body> +<a>Simple test page without favicon (meaning no separate request from http server)</a> +</body> +</html> diff --git a/tests/auto/core/qwebengineurlrequestinterceptor/tst_qwebengineurlrequestinterceptor.cpp b/tests/auto/core/qwebengineurlrequestinterceptor/tst_qwebengineurlrequestinterceptor.cpp index 199eb4eb6..54546569f 100644 --- a/tests/auto/core/qwebengineurlrequestinterceptor/tst_qwebengineurlrequestinterceptor.cpp +++ b/tests/auto/core/qwebengineurlrequestinterceptor/tst_qwebengineurlrequestinterceptor.cpp @@ -64,7 +64,7 @@ private Q_SLOTS: void requestInterceptorByResourceType_data(); void requestInterceptorByResourceType(); void firstPartyUrlHttp(); - void passRefererHeader(); + void customHeaders(); void initiator(); void jsServiceWorker(); }; @@ -107,35 +107,39 @@ struct RequestInfo { int resourceType; }; -static const QByteArray kHttpHeaderReferrerValue = QByteArrayLiteral("http://somereferrer.com/"); -static const QByteArray kHttpHeaderRefererName = QByteArrayLiteral("referer"); static const QUrl kRedirectUrl = QUrl("qrc:///resources/content.html"); +Q_LOGGING_CATEGORY(lc, "qt.webengine.tests") + class TestRequestInterceptor : public QWebEngineUrlRequestInterceptor { public: QList<RequestInfo> requestInfos; bool shouldRedirect = false; + QUrl redirectUrl; QMap<QUrl, QSet<QUrl>> requestInitiatorUrls; QMap<QByteArray, QByteArray> headers; void interceptRequest(QWebEngineUrlRequestInfo &info) override { QVERIFY(QThread::currentThread() == QCoreApplication::instance()->thread()); + qCDebug(lc) << this << "Type:" << info.resourceType() << info.requestMethod() << "Navigation:" << info.navigationType() + << info.requestUrl() << "Initiator:" << info.initiator(); + // Since 63 we also intercept some unrelated blob requests.. if (info.requestUrl().scheme() == QLatin1String("blob")) return; bool block = info.requestMethod() != QByteArrayLiteral("GET"); - bool redirect = shouldRedirect && info.requestUrl() != kRedirectUrl; + bool redirect = shouldRedirect && info.requestUrl() != redirectUrl; + + // set additional headers if any required by test + for (auto it = headers.begin(); it != headers.end(); ++it) info.setHttpHeader(it.key(), it.value()); if (block) { info.block(true); } else if (redirect) { - info.redirect(kRedirectUrl); - } else { - // set additional headers if any required by test - for (auto it = headers.begin(); it != headers.end(); ++it) info.setHttpHeader(it.key(), it.value()); + info.redirect(redirectUrl); } requestInitiatorUrls[info.requestUrl()].insert(info.initiator()); @@ -185,8 +189,8 @@ public: return false; } - TestRequestInterceptor(bool redirect) - : shouldRedirect(redirect) + TestRequestInterceptor(bool redirect = false, const QUrl &url = kRedirectUrl) + : shouldRedirect(redirect), redirectUrl(url) { } }; @@ -574,37 +578,63 @@ void tst_QWebEngineUrlRequestInterceptor::firstPartyUrlHttp() QCOMPARE(info.firstPartyUrl, firstPartyUrl); } -void tst_QWebEngineUrlRequestInterceptor::passRefererHeader() +void tst_QWebEngineUrlRequestInterceptor::customHeaders() { // Create HTTP Server to parse the request. HttpServer httpServer; - - if (!httpServer.start()) - QSKIP("Failed to start http server"); - - bool succeeded = false; - connect(&httpServer, &HttpServer::newRequest, [&succeeded](HttpReqRep *rr) { - const QByteArray headerValue = rr->requestHeader(kHttpHeaderRefererName); - QCOMPARE(headerValue, kHttpHeaderReferrerValue); - succeeded = headerValue == kHttpHeaderReferrerValue; - rr->sendResponse(); - }); + httpServer.setResourceDirs({ TESTS_SOURCE_DIR "qwebengineurlrequestinterceptor/resources" }); + QVERIFY(httpServer.start()); QWebEngineProfile profile; TestRequestInterceptor interceptor(false); - interceptor.headers.insert(kHttpHeaderRefererName, kHttpHeaderReferrerValue); profile.setUrlRequestInterceptor(&interceptor); QWebEnginePage page(&profile); QSignalSpy spy(&page, SIGNAL(loadFinished(bool))); - QWebEngineHttpRequest httpRequest; - QUrl requestUrl = httpServer.url(); - httpRequest.setUrl(requestUrl); - page.load(httpRequest); + interceptor.headers = { + { "referer", "http://somereferrer.com/" }, + { "from", "user@example.com" }, + { "user-agent", "mozilla/5.0 (x11; linux x86_64; rv:12.0) gecko/20100101 firefox/12.0" }, + }; + + QMap<QByteArray, QByteArray> actual, expected; + connect(&httpServer, &HttpServer::newRequest, [&] (HttpReqRep *rr) { + for (auto it = expected.begin(); it != expected.end(); ++it) { + auto headerValue = rr->requestHeader(it.key()); + actual[it.key()] = headerValue; + QCOMPARE(headerValue, it.value()); + } + }); + + auto dumpHeaders = [&] () { + QString s; QDebug d(&s); + for (auto it = expected.begin(); it != expected.end(); ++it) + d << "\n\tHeader:" << it.key() << "| actual:" << actual[it.key()] << "expected:" << it.value(); + return s; + }; + + expected = interceptor.headers; + page.load(httpServer.url("/content.html")); QVERIFY(spy.wait()); + QVERIFY2(actual == expected, qPrintable(dumpHeaders())); + + // test that custom headers are also applied on redirect + interceptor.shouldRedirect = true; + interceptor.redirectUrl = httpServer.url("/content2.html"); + interceptor.headers = { + { "referer", "http://somereferrer2.com/" }, + { "from", "user2@example.com" }, + { "user-agent", "mozilla/5.0 (compatible; googlebot/2.1; +http://www.google.com/bot.html)" }, + }; + + actual.clear(); + expected = interceptor.headers; + page.triggerAction(QWebEnginePage::Reload); + QVERIFY(spy.wait()); + QVERIFY2(actual == expected, qPrintable(dumpHeaders())); + (void) httpServer.stop(); - QVERIFY(succeeded); } void tst_QWebEngineUrlRequestInterceptor::initiator() diff --git a/tests/auto/quick/qmltests/BLACKLIST b/tests/auto/quick/qmltests/BLACKLIST deleted file mode 100644 index 46bc65923..000000000 --- a/tests/auto/quick/qmltests/BLACKLIST +++ /dev/null @@ -1,2 +0,0 @@ -[WebEngineViewSource::test_viewSourceURL] -* diff --git a/tests/auto/quick/qmltests/data/TestWebEngineView.qml b/tests/auto/quick/qmltests/data/TestWebEngineView.qml index 6db076ae8..f2bc09e4b 100644 --- a/tests/auto/quick/qmltests/data/TestWebEngineView.qml +++ b/tests/auto/quick/qmltests/data/TestWebEngineView.qml @@ -85,12 +85,14 @@ WebEngineView { function getElementCenter(element) { var center; - runJavaScript("(function() {" + + testCase.tryVerify(function() { + runJavaScript("(function() {" + " var elem = document.getElementById('" + element + "');" + " var rect = elem.getBoundingClientRect();" + " return { 'x': (rect.left + rect.right) / 2, 'y': (rect.top + rect.bottom) / 2 };" + "})();", function(result) { center = result } ); - testCase.tryVerify(function() { return center !== undefined; }); + return center !== undefined; + }); return center; } diff --git a/tests/auto/quick/qmltests/data/test2.html b/tests/auto/quick/qmltests/data/test2.html index 629c2a063..7a02bf1f2 100644 --- a/tests/auto/quick/qmltests/data/test2.html +++ b/tests/auto/quick/qmltests/data/test2.html @@ -1,6 +1,6 @@ <html> <head><title>Test page with huge link area</title></head> <body> -<a title="A title" href="test1.html"><img width=200 height=200></a> +<a id="link" title="A title" href="test1.html"><img width=200 height=200></a> </body> </html> diff --git a/tests/auto/quick/qmltests/data/tst_loadUrl.qml b/tests/auto/quick/qmltests/data/tst_loadUrl.qml index 872c46641..47dbbc087 100644 --- a/tests/auto/quick/qmltests/data/tst_loadUrl.qml +++ b/tests/auto/quick/qmltests/data/tst_loadUrl.qml @@ -301,8 +301,8 @@ TestWebEngineView { // In-page navigation. webEngineView.url = Qt.resolvedUrl("test4.html#content"); // In-page navigation doesn't trigger load succeeded, wait for load progress instead. + tryCompare(loadRequestArray, "length", 3); tryCompare(webEngineView, "loadProgress", 100); - compare(loadRequestArray.length, 3); compare(loadRequestArray[2].status, WebEngineView.LoadStartedStatus); // Load after in-page navigation. diff --git a/tests/auto/quick/qmltests/data/tst_newViewRequest.qml b/tests/auto/quick/qmltests/data/tst_newViewRequest.qml index 80389e9f8..08d63d956 100644 --- a/tests/auto/quick/qmltests/data/tst_newViewRequest.qml +++ b/tests/auto/quick/qmltests/data/tst_newViewRequest.qml @@ -38,6 +38,13 @@ TestWebEngineView { property var newViewRequest: null property var dialog: null property string viewType: "" + property var loadRequestArray: [] + + onLoadingChanged: { + loadRequestArray.push({ + "status": loadRequest.status, + }); + } SignalSpy { id: newViewRequestedSpy @@ -81,6 +88,7 @@ TestWebEngineView { newViewRequestedSpy.clear(); newViewRequest = null; viewType = ""; + loadRequestArray = []; } function cleanup() { @@ -163,6 +171,23 @@ TestWebEngineView { } newViewRequestedSpy.clear(); } + + loadRequestArray = []; + compare(loadRequestArray.length, 0); + webEngineView.url = Qt.resolvedUrl("test2.html"); + verify(webEngineView.waitForLoadSucceeded()); + var center = getElementCenter("link"); + mouseClick(webEngineView, center.x, center.y, Qt.LeftButton, Qt.ControlModifier); + tryCompare(newViewRequestedSpy, "count", 1); + compare(newViewRequest.requestedUrl, Qt.resolvedUrl("test1.html")); + compare(newViewRequest.destination, WebEngineView.NewViewInBackgroundTab); + verify(newViewRequest.userInitiated); + if (viewType === "" || viewType === "null") { + compare(loadRequestArray[0].status, WebEngineView.LoadStartedStatus); + compare(loadRequestArray[1].status, WebEngineView.LoadSucceededStatus); + compare(loadRequestArray.length, 2); + } + newViewRequestedSpy.clear(); } } } diff --git a/tests/auto/quick/qmltests/data/tst_viewSource.qml b/tests/auto/quick/qmltests/data/tst_viewSource.qml index 4966a052a..22c340c2b 100644 --- a/tests/auto/quick/qmltests/data/tst_viewSource.qml +++ b/tests/auto/quick/qmltests/data/tst_viewSource.qml @@ -94,36 +94,6 @@ TestWebEngineView { compare(webEngineView.url, "view-source:" + Qt.resolvedUrl("test1.html")); } - function test_viewSourceURL_data() { - var testLocalUrl = "view-source:" + Qt.resolvedUrl("test1.html"); - var testLocalUrlWithoutScheme = "view-source:" + Qt.resolvedUrl("test1.html").substring(7); - - return [ - { tag: "view-source:", userInputUrl: "view-source:", loadSucceed: true, url: "view-source:", title: "view-source:" }, - { tag: "view-source:about:blank", userInputUrl: "view-source:about:blank", loadSucceed: true, url: "view-source:about:blank", title: "view-source:about:blank" }, - { tag: testLocalUrl, userInputUrl: testLocalUrl, loadSucceed: true, url: testLocalUrl, title: "test1.html" }, - { tag: testLocalUrlWithoutScheme, userInputUrl: testLocalUrlWithoutScheme, loadSucceed: true, url: testLocalUrl, title: "test1.html" }, - { tag: "view-source:http://non.existent", userInputUrl: "view-source:http://non.existent", loadSucceed: false, url: "http://non.existent/", title: "non.existent" }, - { tag: "view-source:non.existent", userInputUrl: "view-source:non.existent", loadSucceed: false, url: "http://non.existent/", title: "non.existent" }, - ]; - } - - function test_viewSourceURL(row) { - WebEngine.settings.errorPageEnabled = true - webEngineView.url = row.userInputUrl; - - if (row.loadSucceed) { - tryCompare(webEngineView, "loadStatus", WebEngineView.LoadSucceededStatus); - } else { - tryCompare(webEngineView, "loadStatus", WebEngineView.LoadFailedStatus, 15000); - } - tryVerify(function() { return titleChangedSpy.count == 1; }); - - compare(webEngineView.url, row.url); - tryCompare(webEngineView, "title", row.title); - verify(!webEngineView.action(WebEngineView.ViewSource).enabled); - } - function test_viewSourceCredentials() { var url = "http://user:passwd@httpbin.org/basic-auth/user/passwd"; diff --git a/tests/auto/quick/qmltests/data/tst_viewSoure.qml b/tests/auto/quick/qmltests/data/tst_viewSoure.qml new file mode 100644 index 000000000..997582335 --- /dev/null +++ b/tests/auto/quick/qmltests/data/tst_viewSoure.qml @@ -0,0 +1,133 @@ +/**************************************************************************** +** +** Copyright (C) 2020 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$ +** +****************************************************************************/ + +import QtQuick 2.0 +import QtTest 1.0 +import QtWebEngine 1.4 +import QtWebEngine.testsupport 1.0 +import "../../qmltests/data" 1.0 + +TestWebEngineView { + id: webEngineView + width: 200 + height: 400 + + property var viewRequest: null + property var loadRequestArray: [] + + testSupport: WebEngineTestSupport { + errorPage.onLoadingChanged: { + loadRequestArray.push({ + "status": loadRequest.status, + }) + } + } + + onLoadingChanged: { + loadRequestArray.push({ + "status": loadRequest.status, + }); + } + + SignalSpy { + id: newViewRequestedSpy + target: webEngineView + signalName: "newViewRequested" + } + + SignalSpy { + id: titleChangedSpy + target: webEngineView + signalName: "titleChanged" + } + + onNewViewRequested: { + viewRequest = { + "destination": request.destination, + "userInitiated": request.userInitiated + }; + + request.openIn(webEngineView); + } + + TestCase { + id: test + name: "WebEngineViewSource" + + function init() { + webEngineView.loadStatus = null; + webEngineView.url = Qt.resolvedUrl("test1.html"); + tryCompare(webEngineView, "loadStatus", WebEngineView.LoadSucceededStatus); + webEngineView.loadStatus = null; + + newViewRequestedSpy.clear(); + titleChangedSpy.clear(); + viewRequest = null; + } + + function test_viewSourceURL_data() { + var testLocalUrl = "view-source:" + Qt.resolvedUrl("test1.html"); + var testLocalUrlWithoutScheme = "view-source:" + Qt.resolvedUrl("test1.html").toString().substring(7); + + return [ + { tag: "view-source:", userInputUrl: "view-source:", loadSucceed: true, url: "view-source:", title: "view-source:" }, + { tag: "view-source:about:blank", userInputUrl: "view-source:about:blank", loadSucceed: true, url: "view-source:about:blank", title: "view-source:about:blank" }, + { tag: testLocalUrl, userInputUrl: testLocalUrl, loadSucceed: true, url: testLocalUrl, title: "test1.html" }, + { tag: testLocalUrlWithoutScheme, userInputUrl: testLocalUrlWithoutScheme, loadSucceed: true, url: testLocalUrl, title: "test1.html" }, + { tag: "view-source:http://non.existent", userInputUrl: "view-source:http://non.existent", loadSucceed: false, url: "http://non.existent/", title: "non.existent" }, + { tag: "view-source:non.existent", userInputUrl: "view-source:non.existent", loadSucceed: false, url: "http://non.existent/", title: "non.existent" }, + ]; + } + + function test_viewSourceURL(row) { + loadRequestArray = []; + WebEngine.settings.errorPageEnabled = true + webEngineView.url = row.userInputUrl; + + if (row.loadSucceed) { + tryVerify(function() { return loadRequestArray.length >= 2 }); + compare(loadRequestArray[1].status, WebEngineView.LoadSucceededStatus); + } else { + tryVerify(function() { return loadRequestArray.length >= 2 }); + compare(loadRequestArray[1].status, WebEngineView.LoadFailedStatus); + tryVerify(function() { return loadRequestArray.length == 4 }); + compare(loadRequestArray[3].status, WebEngineView.LoadSucceededStatus); + } + tryVerify(function() { return titleChangedSpy.count == 1; }); + + compare(webEngineView.url, row.url); + tryCompare(webEngineView, "title", row.title); + if (row.loadSucceed) { + verify(!webEngineView.action(WebEngineView.ViewSource).enabled); + } else { + verify(webEngineView.action(WebEngineView.ViewSource).enabled); + } + } + } +} + diff --git a/tests/auto/quick/qmltests/qmltests.pro b/tests/auto/quick/qmltests/qmltests.pro index eb53a98bb..5b2ea5d01 100644 --- a/tests/auto/quick/qmltests/qmltests.pro +++ b/tests/auto/quick/qmltests/qmltests.pro @@ -55,7 +55,8 @@ qtConfig(webengine-testsupport) { $$PWD/data/tst_inputMethod.qml \ $$PWD/data/tst_linkHovered.qml \ $$PWD/data/tst_loadFail.qml \ - $$PWD/data/tst_mouseClick.qml + $$PWD/data/tst_mouseClick.qml \ + $$PWD/data/tst_viewSoure.qml qtHaveModule(quickcontrols): QML_TESTS += $$PWD/data/tst_javaScriptDialogs.qml } else { PLUGIN_EXTENSION = .so diff --git a/tests/auto/quick/qquickwebengineview/tst_qquickwebengineview.cpp b/tests/auto/quick/qquickwebengineview/tst_qquickwebengineview.cpp index c1a95de97..fd801c824 100644 --- a/tests/auto/quick/qquickwebengineview/tst_qquickwebengineview.cpp +++ b/tests/auto/quick/qquickwebengineview/tst_qquickwebengineview.cpp @@ -1001,6 +1001,7 @@ void tst_QQuickWebEngineView::inputEventForwardingDisabledWhenActiveFocusOnPress void tst_QQuickWebEngineView::changeLocale() { +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) QStringList errorLines; QUrl url("http://non.existent/"); @@ -1036,6 +1037,7 @@ void tst_QQuickWebEngineView::changeLocale() QTRY_VERIFY(!evaluateJavaScriptSync(viewDE.data(), "document.body.innerText").isNull()); errorLines = evaluateJavaScriptSync(viewDE.data(), "document.body.innerText").toString().split(QRegularExpression("[\r\n]"), Qt::SkipEmptyParts); QCOMPARE(errorLines.first().toUtf8(), QByteArrayLiteral("Die Website ist nicht erreichbar")); +#endif } void tst_QQuickWebEngineView::userScripts() @@ -1181,6 +1183,9 @@ void tst_QQuickWebEngineView::focusChild_data() void tst_QQuickWebEngineView::focusChild() { +#if QT_VERSION < QT_VERSION_CHECK(5, 14, 1) + QSKIP("Requires newer base Qt"); +#endif auto traverseToWebDocumentAccessibleInterface = [](QAccessibleInterface *iface) -> QAccessibleInterface * { QFETCH(QList<QAccessible::Role>, ancestorRoles); for (int i = 0; i < ancestorRoles.size(); ++i) { diff --git a/tests/auto/shared/httpserver.cpp b/tests/auto/shared/httpserver.cpp index 67f491fac..69e8cb6cc 100644 --- a/tests/auto/shared/httpserver.cpp +++ b/tests/auto/shared/httpserver.cpp @@ -54,6 +54,7 @@ bool HttpServer::start() { m_error = false; m_expectingError = false; + m_ignoreNewConnection = false; if (!m_tcpServer->listen()) { qCWarning(gHttpServerLog).noquote() << m_tcpServer->errorString(); @@ -84,6 +85,9 @@ QUrl HttpServer::url(const QString &path) const void HttpServer::handleNewConnection() { + if (m_ignoreNewConnection) + return; + auto rr = new HttpReqRep(m_tcpServer->nextPendingConnection(), this); connect(rr, &HttpReqRep::requestReceived, [this, rr]() { Q_EMIT newRequest(rr); @@ -122,5 +126,9 @@ void HttpServer::handleNewConnection() << error; m_error = true; }); - connect(rr, &HttpReqRep::closed, rr, &QObject::deleteLater); + + if (!m_tcpServer->isListening()) { + m_ignoreNewConnection = true; + connect(rr, &HttpReqRep::closed, rr, &QObject::deleteLater); + } } diff --git a/tests/auto/shared/httpserver.h b/tests/auto/shared/httpserver.h index 9764852de..952ead220 100644 --- a/tests/auto/shared/httpserver.h +++ b/tests/auto/shared/httpserver.h @@ -90,6 +90,7 @@ private: QUrl m_url; QStringList m_dirs; bool m_error = false; + bool m_ignoreNewConnection = false; bool m_expectingError = false; }; diff --git a/tests/auto/widgets/accessibility/tst_accessibility.cpp b/tests/auto/widgets/accessibility/tst_accessibility.cpp index e73f7d89b..989ad0ee9 100644 --- a/tests/auto/widgets/accessibility/tst_accessibility.cpp +++ b/tests/auto/widgets/accessibility/tst_accessibility.cpp @@ -160,6 +160,9 @@ void tst_Accessibility::focusChild_data() void tst_Accessibility::focusChild() { +#if QT_VERSION < QT_VERSION_CHECK(5, 14, 1) + QSKIP("Requires newer base Qt"); +#endif auto traverseToWebDocumentAccessibleInterface = [](QAccessibleInterface *iface) -> QAccessibleInterface * { QFETCH(QList<QAccessible::Role>, ancestorRoles); for (int i = 0; i < ancestorRoles.size(); ++i) { diff --git a/tests/auto/widgets/loadsignals/tst_loadsignals.cpp b/tests/auto/widgets/loadsignals/tst_loadsignals.cpp index 143a4a4e5..c1e9013df 100644 --- a/tests/auto/widgets/loadsignals/tst_loadsignals.cpp +++ b/tests/auto/widgets/loadsignals/tst_loadsignals.cpp @@ -52,6 +52,7 @@ private Q_SLOTS: void secondLoadForError_WhenErrorPageEnabled(); void loadAfterInPageNavigation_qtbug66869(); void fileDownloadDoesNotTriggerLoadSignals_qtbug66661(); + void numberOfStartedAndFinishedSignalsIsSame(); private: QWebEngineProfile profile; @@ -243,5 +244,44 @@ void tst_LoadSignals::fileDownloadDoesNotTriggerLoadSignals_qtbug66661() QCOMPARE(loadFinishedSpy.size(), 1); } +void tst_LoadSignals::numberOfStartedAndFinishedSignalsIsSame() { + + HttpServer server; + server.setResourceDirs({ TESTS_SOURCE_DIR "/qwebengineprofile/resources" }); + connect(&server, &HttpServer::newRequest, [] (HttpReqRep *) { + QTest::qWait(250); // just add delay to trigger some progress for every sub resource + }); + QVERIFY(server.start()); + + view.load(server.url("/hedgehog.png")); + QTRY_COMPARE(loadFinishedSpy.size(), 1); + QVERIFY(loadFinishedSpy[0][0].toBool()); + + loadStartedSpy.clear(); + loadFinishedSpy.clear(); + loadProgressSpy.clear(); + + view.page()->setHtml("<html><body>" + "<img src=\"" + server.url("/hedgehog.png").toEncoded() + "\">" + "<form method='GET' name='hiddenform' action='qrc:///resources/page1.html' />" + "<script language='javascript'>document.forms[0].submit();</script>" + "</body></html>"); + + QTRY_COMPARE(loadStartedSpy.size(), 2); + QTRY_COMPARE(loadFinishedSpy.size(), 2); + + QTRY_VERIFY(!loadFinishedSpy[0][0].toBool()); + QTRY_VERIFY(loadFinishedSpy[1][0].toBool()); + + view.page()->setHtml("<html><body>" + "<form method='GET' name='hiddenform' action='qrc:///resources/page1.html' />" + "<script language='javascript'>document.forms[0].submit();</script>" + "</body></html>"); + QTRY_COMPARE(loadStartedSpy.size(), 4); + QTRY_COMPARE(loadFinishedSpy.size(), 4); + QVERIFY(loadFinishedSpy[2][0].toBool()); + QVERIFY(loadFinishedSpy[3][0].toBool()); +} + QTEST_MAIN(tst_LoadSignals) #include "tst_loadsignals.moc" diff --git a/tests/auto/widgets/qwebenginehistory/tst_qwebenginehistory.cpp b/tests/auto/widgets/qwebenginehistory/tst_qwebenginehistory.cpp index bdb486793..72a45379b 100644 --- a/tests/auto/widgets/qwebenginehistory/tst_qwebenginehistory.cpp +++ b/tests/auto/widgets/qwebenginehistory/tst_qwebenginehistory.cpp @@ -320,7 +320,8 @@ void tst_QWebEngineHistory::serialize_2() hist->forward(); QTRY_COMPARE(loadFinishedSpy->count(), 5); hist->forward(); - QTRY_COMPARE(loadFinishedSpy->count(), 6); + // In-page navigation, the last url was the page5.html + QTRY_COMPARE(loadFinishedSpy->count(), 5); QTRY_COMPARE(hist->currentItemIndex(), initialCurrentIndex); } diff --git a/tests/auto/widgets/qwebenginepage/tst_qwebenginepage.cpp b/tests/auto/widgets/qwebenginepage/tst_qwebenginepage.cpp index a7e594d14..41ec977ab 100644 --- a/tests/auto/widgets/qwebenginepage/tst_qwebenginepage.cpp +++ b/tests/auto/widgets/qwebenginepage/tst_qwebenginepage.cpp @@ -3468,7 +3468,11 @@ void tst_QWebEnginePage::openLinkInNewPage_data() // the disposition and performing the navigation request normally. QTest::newRow("BlockPopup") << Decision::ReturnNull << Cause::TargetBlank << Effect::Blocked; +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + QTest::newRow("IgnoreIntent") << Decision::ReturnNull << Cause::MiddleClick << Effect::Blocked; +#else QTest::newRow("IgnoreIntent") << Decision::ReturnNull << Cause::MiddleClick << Effect::LoadInSelf; +#endif QTest::newRow("OverridePopup") << Decision::ReturnSelf << Cause::TargetBlank << Effect::LoadInSelf; QTest::newRow("OverrideIntent") << Decision::ReturnSelf << Cause::MiddleClick << Effect::LoadInSelf; QTest::newRow("AcceptPopup") << Decision::ReturnOther << Cause::TargetBlank << Effect::LoadInOther; @@ -3545,7 +3549,10 @@ void tst_QWebEnginePage::openLinkInNewPage() switch (effect) { case Effect::Blocked: - // Nothing to test + // Test nothing new loaded + QTest::qWait(500); + QCOMPARE(page1.spy.count(), 0); + QCOMPARE(page2.spy.count(), 0); break; case Effect::LoadInSelf: QTRY_COMPARE(page1.spy.count(), 1); diff --git a/tests/auto/widgets/qwebengineview/BLACKLIST b/tests/auto/widgets/qwebengineview/BLACKLIST index 266f08886..1aff12669 100644 --- a/tests/auto/widgets/qwebengineview/BLACKLIST +++ b/tests/auto/widgets/qwebengineview/BLACKLIST @@ -1,8 +1,5 @@ [microFocusCoordinates] osx -[textSelectionOutOfInputField] -* - [visibilityState3] windows diff --git a/tests/auto/widgets/qwebengineview/tst_qwebengineview.cpp b/tests/auto/widgets/qwebengineview/tst_qwebengineview.cpp index 87664cef9..1a0f77b78 100644 --- a/tests/auto/widgets/qwebengineview/tst_qwebengineview.cpp +++ b/tests/auto/widgets/qwebengineview/tst_qwebengineview.cpp @@ -25,7 +25,6 @@ #include <private/qinputmethod_p.h> #include <qpainter.h> #include <qpagelayout.h> -#include <qpa/qplatforminputcontext.h> #include <qwebengineview.h> #include <qwebenginepage.h> #include <qwebenginesettings.h> @@ -60,44 +59,6 @@ do { \ QCOMPARE((__expr), __expected); \ } while (0) -static QPointingDevice* s_touchDevice = nullptr; - -static QPoint elementCenter(QWebEnginePage *page, const QString &id) -{ - const QString jsCode( - "(function(){" - " var elem = document.getElementById('" + id + "');" - " var rect = elem.getBoundingClientRect();" - " return [(rect.left + rect.right) / 2, (rect.top + rect.bottom) / 2];" - "})()"); - QVariantList rectList = evaluateJavaScriptSync(page, jsCode).toList(); - - if (rectList.count() != 2) { - qWarning("elementCenter failed."); - return QPoint(); - } - - return QPoint(rectList.at(0).toInt(), rectList.at(1).toInt()); -} - -static QRect elementGeometry(QWebEnginePage *page, const QString &id) -{ - const QString jsCode( - "(function() {" - " var elem = document.getElementById('" + id + "');" - " var rect = elem.getBoundingClientRect();" - " return [rect.left, rect.top, rect.right, rect.bottom];" - "})()"); - QVariantList coords = evaluateJavaScriptSync(page, jsCode).toList(); - - if (coords.count() != 4) { - qWarning("elementGeometry faield."); - return QRect(); - } - - return QRect(coords[0].toInt(), coords[1].toInt(), coords[2].toInt(), coords[3].toInt()); -} - QT_BEGIN_NAMESPACE namespace QTest { int Q_TESTLIB_EXPORT defaultMouseDelay(); @@ -167,9 +128,6 @@ private Q_SLOTS: void keyboardEvents(); void keyboardFocusAfterPopup(); void mouseClick(); - void touchTap(); - void touchTapAndHold(); - void touchTapAndHoldCancelled(); void postData(); void inputFieldOverridesShortcuts(); @@ -216,7 +174,6 @@ private Q_SLOTS: // It is only called once. void tst_QWebEngineView::initTestCase() { - s_touchDevice = QTest::createTouchDevice(); } // This will be called after the last test function is executed. @@ -1203,6 +1160,7 @@ void tst_QWebEngineView::doNotBreakLayout() void tst_QWebEngineView::changeLocale() { +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) QStringList errorLines; QUrl url("http://non.existent/"); @@ -1238,6 +1196,7 @@ void tst_QWebEngineView::changeLocale() QTRY_VERIFY(!toPlainTextSync(viewDE.page()).isEmpty()); errorLines = toPlainTextSync(viewDE.page()).split(QRegularExpression("[\r\n]"), Qt::SkipEmptyParts); QCOMPARE(errorLines.first().toUtf8(), QByteArrayLiteral("Die Website ist nicht erreichbar")); +#endif } void tst_QWebEngineView::inputMethodsTextFormat_data() @@ -1283,6 +1242,7 @@ void tst_QWebEngineView::inputMethodsTextFormat() evaluateJavaScriptSync(view.page(), "document.getElementById('input1').focus()"); view.show(); + QVERIFY(QTest::qWaitForWindowExposed(&view)); QFETCH(QString, string); QFETCH(int, start); @@ -1520,174 +1480,9 @@ void tst_QWebEngineView::mouseClick() QVERIFY(view.focusProxy()->inputMethodQuery(Qt::ImCurrentSelection).toString().isEmpty()); } -void tst_QWebEngineView::touchTap() -{ -#if defined(Q_OS_MACOS) - QSKIP("Synthetic touch events are not supported on macOS"); -#endif - - QWebEngineView view; - view.show(); - view.resize(200, 200); - QVERIFY(QTest::qWaitForWindowExposed(&view)); - - QSignalSpy loadFinishedSpy(&view, &QWebEngineView::loadFinished); - - view.settings()->setAttribute(QWebEngineSettings::FocusOnNavigationEnabled, false); - view.setHtml("<html><body>" - "<p id='text' style='width: 150px;'>The Qt Company</p>" - "<div id='notext' style='width: 150px; height: 100px; background-color: #f00;'></div>" - "<form><input id='input' width='150px' type='text' value='The Qt Company2' /></form>" - "</body></html>"); - QVERIFY(loadFinishedSpy.wait()); - QVERIFY(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString().isEmpty()); - - auto singleTap = [](QWidget* target, const QPoint& tapCoords) -> void { - QTest::touchEvent(target->window(), s_touchDevice).press(0, tapCoords, target); - QTest::touchEvent(target->window(), s_touchDevice).stationary(0); - QTest::touchEvent(target->window(), s_touchDevice).release(0, tapCoords, target); - }; - - // Single tap on text doesn't trigger a selection - singleTap(view.focusProxy(), elementCenter(view.page(), "text")); - QTRY_VERIFY(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString().isEmpty()); - QTRY_VERIFY(!view.hasSelection()); - - // Single tap inside the input field focuses it without selecting the text - singleTap(view.focusProxy(), elementCenter(view.page(), "input")); - QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString(), QStringLiteral("input")); - QTRY_VERIFY(!view.hasSelection()); - - // Single tap on the div clears the input field focus - singleTap(view.focusProxy(), elementCenter(view.page(), "notext")); - QTRY_VERIFY(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString().isEmpty()); - - // Double tap on text still doesn't trigger a selection - singleTap(view.focusProxy(), elementCenter(view.page(), "text")); - singleTap(view.focusProxy(), elementCenter(view.page(), "text")); - QTRY_VERIFY(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString().isEmpty()); - QTRY_VERIFY(!view.hasSelection()); - - // Double tap inside the input field focuses it and selects the word under it - singleTap(view.focusProxy(), elementCenter(view.page(), "input")); - singleTap(view.focusProxy(), elementCenter(view.page(), "input")); - QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString(), QStringLiteral("input")); - QTRY_COMPARE(view.selectedText(), QStringLiteral("Company2")); - - // Double tap outside the input field behaves like a single tap: clears its focus and selection - singleTap(view.focusProxy(), elementCenter(view.page(), "notext")); - singleTap(view.focusProxy(), elementCenter(view.page(), "notext")); - QTRY_VERIFY(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString().isEmpty()); - QTRY_VERIFY(!view.hasSelection()); -} - -void tst_QWebEngineView::touchTapAndHold() -{ -#if defined(Q_OS_MACOS) - QSKIP("Synthetic touch events are not supported on macOS"); -#endif - - QWebEngineView view; - view.show(); - view.resize(200, 200); - QVERIFY(QTest::qWaitForWindowExposed(&view)); - - QSignalSpy loadFinishedSpy(&view, &QWebEngineView::loadFinished); - - view.settings()->setAttribute(QWebEngineSettings::FocusOnNavigationEnabled, false); - view.setHtml("<html><body>" - "<p id='text' style='width: 150px;'>The Qt Company</p>" - "<div id='notext' style='width: 150px; height: 100px; background-color: #f00;'></div>" - "<form><input id='input' width='150px' type='text' value='The Qt Company2' /></form>" - "</body></html>"); - QVERIFY(loadFinishedSpy.wait()); - QVERIFY(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString().isEmpty()); - - auto tapAndHold = [](QWidget* target, const QPoint& tapCoords) -> void { - QTest::touchEvent(target, s_touchDevice).press(0, tapCoords, target); - QTest::touchEvent(target, s_touchDevice).stationary(0); - QTest::qWait(1000); - QTest::touchEvent(target, s_touchDevice).release(0, tapCoords, target); - }; - - // Tap-and-hold on text selects the word under it - tapAndHold(view.focusProxy(), elementCenter(view.page(), "text")); - QTRY_VERIFY(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString().isEmpty()); - QTRY_COMPARE(view.selectedText(), QStringLiteral("Company")); - - // Tap-and-hold inside the input field focuses it and selects the word under it - tapAndHold(view.focusProxy(), elementCenter(view.page(), "input")); - QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString(), QStringLiteral("input")); - QTRY_COMPARE(view.selectedText(), QStringLiteral("Company2")); - - // Only test the page context menu on Windows, as Linux doesn't handle context menus consistently - // and other non-desktop platforms like Android may not even support context menus at all -#if defined(Q_OS_WIN) - // Tap-and-hold clears the text selection and shows the page's context menu - QVERIFY(QApplication::activePopupWidget() == nullptr); - tapAndHold(view.focusProxy(), elementCenter(view.page(), "notext")); - QTRY_VERIFY(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString().isEmpty()); - QTRY_VERIFY(!view.hasSelection()); - QTRY_VERIFY(QApplication::activePopupWidget() != nullptr); - - QApplication::activePopupWidget()->close(); - QVERIFY(QApplication::activePopupWidget() == nullptr); -#endif -} - -void tst_QWebEngineView::touchTapAndHoldCancelled() -{ -#if defined(Q_OS_MACOS) - QSKIP("Synthetic touch events are not supported on macOS"); -#endif - - QWebEngineView view; - view.show(); - view.resize(200, 200); - QVERIFY(QTest::qWaitForWindowExposed(&view)); - - QSignalSpy loadFinishedSpy(&view, &QWebEngineView::loadFinished); - - view.settings()->setAttribute(QWebEngineSettings::FocusOnNavigationEnabled, false); - view.setHtml("<html><body>" - "<p id='text' style='width: 150px;'>The Qt Company</p>" - "<div id='notext' style='width: 150px; height: 100px; background-color: #f00;'></div>" - "<form><input id='input' width='150px' type='text' value='The Qt Company2' /></form>" - "</body></html>"); - QVERIFY(loadFinishedSpy.wait()); - QVERIFY(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString().isEmpty()); - - auto cancelledTapAndHold = [](QWidget* target, const QPoint& tapCoords) -> void { - QTest::touchEvent(target, s_touchDevice).press(1, tapCoords, target); - QTest::touchEvent(target, s_touchDevice).stationary(1); - QTest::qWait(1000); - QWindowSystemInterface::handleTouchCancelEvent(target->windowHandle(), s_touchDevice); - }; - - // A cancelled tap-and-hold should cancel text selection, but currently doesn't - cancelledTapAndHold(view.focusProxy(), elementCenter(view.page(), "text")); - QEXPECT_FAIL("", "Incorrect Chromium selection behavior when cancelling tap-and-hold on text", Continue); - QTRY_VERIFY_WITH_TIMEOUT(!view.hasSelection(), 100); - - // A cancelled tap-and-hold should cancel input field focusing and selection, but currently doesn't - cancelledTapAndHold(view.focusProxy(), elementCenter(view.page(), "input")); - QEXPECT_FAIL("", "Incorrect Chromium selection behavior when cancelling tap-and-hold on input field", Continue); - QTRY_VERIFY_WITH_TIMEOUT(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString().isEmpty(), 100); - QEXPECT_FAIL("", "Incorrect Chromium focus behavior when cancelling tap-and-hold on input field", Continue); - QTRY_VERIFY_WITH_TIMEOUT(!view.hasSelection(), 100); - - // Only test the page context menu on Windows, as Linux doesn't handle context menus consistently - // and other non-desktop platforms like Android may not even support context menus at all -#if defined(Q_OS_WIN) - // A cancelled tap-and-hold cancels the context menu - QVERIFY(QApplication::activePopupWidget() == nullptr); - cancelledTapAndHold(view.focusProxy(), elementCenter(view.page(), "notext")); - QVERIFY(QApplication::activePopupWidget() == nullptr); -#endif -} - void tst_QWebEngineView::postData() { +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) QMap<QString, QString> postData; // use reserved characters to make the test harder to pass postData[QStringLiteral("Spä=m")] = QStringLiteral("ëgg:s"); @@ -1815,6 +1610,7 @@ void tst_QWebEngineView::postData() timeoutGuard.stop(); server.close(); +#endif } void tst_QWebEngineView::inputFieldOverridesShortcuts() @@ -2051,6 +1847,7 @@ void tst_QWebEngineView::inputContextQueryInput() " <input type='text' id='input1' value='' size='50'/>" "</body></html>"); QTRY_COMPARE(loadFinishedSpy.count(), 1); + QVERIFY(QTest::qWaitForWindowExposed(&view)); QCOMPARE(testContext.infos.count(), 0); // Set focus on an input field. @@ -2202,6 +1999,7 @@ void tst_QWebEngineView::inputMethods() " <input type='text' id='input1' style='font-family: serif' value='' maxlength='20' size='50'/>" "</body></html>"); QTRY_COMPARE(loadFinishedSpy.size(), 1); + QVERIFY(QTest::qWaitForWindowExposed(&view)); QPoint textInputCenter = elementCenter(view.page(), "input1"); QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, textInputCenter); @@ -2299,6 +2097,7 @@ void tst_QWebEngineView::textSelectionInInputField() " <input type='text' id='input1' value='QtWebEngine' size='50'/>" "</body></html>"); QVERIFY(loadFinishedSpy.wait()); + QVERIFY(QTest::qWaitForWindowExposed(&view)); // Tests for Selection when the Editor is NOT in Composition mode @@ -2372,6 +2171,7 @@ void tst_QWebEngineView::textSelectionInInputField() void tst_QWebEngineView::textSelectionOutOfInputField() { QWebEngineView view; + view.settings()->setAttribute(QWebEngineSettings::FocusOnNavigationEnabled, true); view.resize(640, 480); view.show(); @@ -2381,6 +2181,7 @@ void tst_QWebEngineView::textSelectionOutOfInputField() " This is a text" "</body></html>"); QVERIFY(loadFinishedSpy.wait()); + QVERIFY(QTest::qWaitForWindowExposed(&view)); QCOMPARE(selectionChangedSpy.count(), 0); QVERIFY(!view.hasSelection()); @@ -2429,6 +2230,7 @@ void tst_QWebEngineView::textSelectionOutOfInputField() " <input type='text' id='input1' value='QtWebEngine' size='50'/>" "</body></html>"); QVERIFY(loadFinishedSpy.wait()); + QVERIFY(QTest::qWaitForWindowExposed(&view)); QCOMPARE(selectionChangedSpy.count(), 0); QVERIFY(!view.hasSelection()); @@ -2508,6 +2310,7 @@ void tst_QWebEngineView::emptyInputMethodEvent() " <input type='text' id='input1' value='QtWebEngine'/>" "</body></html>"); QVERIFY(loadFinishedSpy.wait()); + QVERIFY(QTest::qWaitForWindowExposed(&view)); evaluateJavaScriptSync(view.page(), "var inputEle = document.getElementById('input1'); inputEle.focus(); inputEle.select();"); QTRY_COMPARE(selectionChangedSpy.count(), 1); @@ -2556,6 +2359,7 @@ void tst_QWebEngineView::imeComposition() " <input type='text' id='input1' value='QtWebEngine inputMethod'/>" "</body></html>"); QVERIFY(loadFinishedSpy.wait()); + QVERIFY(QTest::qWaitForWindowExposed(&view)); evaluateJavaScriptSync(view.page(), "var inputEle = document.getElementById('input1'); inputEle.focus(); inputEle.select();"); QTRY_COMPARE(selectionChangedSpy.count(), 1); @@ -2773,6 +2577,7 @@ void tst_QWebEngineView::newlineInTextarea() " <textarea rows='5' cols='1' id='input1'></textarea>" "</body></html>"); QVERIFY(loadFinishedSpy.wait()); + QVERIFY(QTest::qWaitForWindowExposed(&view)); evaluateJavaScriptSync(view.page(), "var inputEle = document.getElementById('input1'); inputEle.focus(); inputEle.select();"); QTRY_VERIFY(evaluateJavaScriptSync(view.page(), "document.getElementById('input1').value").toString().isEmpty()); @@ -2897,6 +2702,7 @@ void tst_QWebEngineView::imeJSInputEvents() " <pre id='log'></pre>" "</body></html>"); QVERIFY(loadFinishedSpy.wait()); + QVERIFY(QTest::qWaitForWindowExposed(&view)); evaluateJavaScriptSync(view.page(), "document.getElementById('input').focus()"); QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString(), QStringLiteral("input")); @@ -3019,6 +2825,7 @@ void tst_QWebEngineView::imeCompositionQueryEvent() " <input type='text' id='input1' />" "</body></html>"); QVERIFY(loadFinishedSpy.wait()); + QVERIFY(QTest::qWaitForWindowExposed(&view)); evaluateJavaScriptSync(view.page(), "document.getElementById('input1').focus()"); QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString(), QStringLiteral("input1")); diff --git a/tests/auto/widgets/touchinput/touchinput.pro b/tests/auto/widgets/touchinput/touchinput.pro new file mode 100644 index 000000000..d91c0074b --- /dev/null +++ b/tests/auto/widgets/touchinput/touchinput.pro @@ -0,0 +1,2 @@ +include(../tests.pri) +QT *= gui-private diff --git a/tests/auto/widgets/touchinput/tst_touchinput.cpp b/tests/auto/widgets/touchinput/tst_touchinput.cpp new file mode 100644 index 000000000..6f22e8df8 --- /dev/null +++ b/tests/auto/widgets/touchinput/tst_touchinput.cpp @@ -0,0 +1,341 @@ +/**************************************************************************** +** +** Copyright (C) 2020 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 "../util.h" + +#include <QtGui/qpa/qwindowsysteminterface.h> +#include <QSignalSpy> +#include <QTest> +#include <QPointingDevice> +#include <QWebEngineSettings> +#include <QWebEngineView> + +static QPointingDevice* s_touchDevice = nullptr; + +class TouchInputTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + +private Q_SLOTS: + void touchTap(); + void touchTapAndHold(); + void touchTapAndHoldCancelled(); + void scrolling(); + void pinchZoom_data(); + void pinchZoom(); + void complexSequence(); + +private: + QWebEngineView view; + QSignalSpy loadSpy { &view, &QWebEngineView::loadFinished }; + QPoint notextCenter, textCenter, inputCenter; + + QString activeElement() { return evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString(); } + + void gestureScroll(bool down) { + auto target = view.focusProxy(); + QPoint p(target->width() / 2, target->height() / 4 * (down ? 3 : 1)); + + QTest::touchEvent(target, s_touchDevice).press(42, p, target); + + for (int i = 0; i < 3; ++i) { + down ? p -= QPoint(5, 15) : p += QPoint(5, 15); + QTest::qWait(100); // too fast and events are recognized as fling gesture + QTest::touchEvent(target, s_touchDevice).move(42, p, target); + } + + QTest::touchEvent(target, s_touchDevice).release(42, p, target); + } + + void gesturePinch(bool zoomIn, bool tapOneByOne = false) { + auto target = view.focusProxy(); + QPoint p(target->width() / 2, target->height() / 2); + auto t1 = p - QPoint(zoomIn ? 50 : 150, 10), t2 = p + QPoint(zoomIn ? 50 : 150, 10); + + if (tapOneByOne) { + QTest::touchEvent(target, s_touchDevice).press(42, t1, target); + QTest::touchEvent(target, s_touchDevice).stationary(42).press(24, t2, target); + } else { + QTest::touchEvent(target, s_touchDevice).press(42, t1, target).press(24, t2, target); + } + + for (int i = 0; i < 3; ++i) { + if (zoomIn) { + t1 -= QPoint(25, 5); + t2 += QPoint(25, 5); + } else { + t1 += QPoint(35, 5); + t2 -= QPoint(35, 5); + } + QTest::qWait(100); // too fast and events are recognized as fling gesture + QTest::touchEvent(target, s_touchDevice).move(24, t1, target).move(42, t2, target); + } + + if (tapOneByOne) { + QTest::touchEvent(target, s_touchDevice).stationary(42).release(24, t2, target); + QTest::touchEvent(target, s_touchDevice).release(42, t1, target); + } else { + QTest::touchEvent(target, s_touchDevice).release(42, t1, target).release(24, t2, target); + } + } + + int getScrollPosition(int *position = nullptr) { + int p = evaluateJavaScriptSync(view.page(), "window.scrollY").toInt(); + return position ? (*position = p) : p; + } + + double getScaleFactor(double *scale = nullptr) { + double s = evaluateJavaScriptSync(view.page(), "window.visualViewport.scale").toDouble(); + return scale ? (*scale = s) : s; + } +}; + +void TouchInputTest::initTestCase() +{ + s_touchDevice = QTest::createTouchDevice(); + + view.settings()->setAttribute(QWebEngineSettings::FocusOnNavigationEnabled, false); + + view.show(); view.resize(480, 320); + QVERIFY(QTest::qWaitForWindowExposed(&view)); + + view.setHtml("<html><head><style>.rect { min-width: 240px; min-height: 120px; }</style></head><body>" + "<p id='text' style='width: 150px;'>The Qt Company</p>" + "<div id='notext' style='width: 150px; height: 100px; background-color: #f00;'></div>" + "<form><input id='input' width='150px' type='text' value='The Qt Company2' /></form>" + "<table style='width: 100%; padding: 15px; text-align: center;'>" + "<tr><td>BEFORE</td><td><div class='rect' style='background-color: #00f;'></div></td><td>AFTER</td></tr>" + "<tr><td>BEFORE</td><td><div class='rect' style='background-color: #0f0;'></div></td><td>AFTER</td></tr>" + "<tr><td>BEFORE</td><td><div class='rect' style='background-color: #f00;'></div></td><td>AFTER</td></tr></table>" + "</body></html>"); + QVERIFY(loadSpy.wait() && loadSpy.first().first().toBool()); + + notextCenter = elementCenter(view.page(), "notext"); + textCenter = elementCenter(view.page(), "text"); + inputCenter = elementCenter(view.page(), "input"); +} + +void TouchInputTest::init() +{ + QCOMPARE(activeElement(), QString()); +} + +void TouchInputTest::cleanup() +{ + evaluateJavaScriptSync(view.page(), "if (document.activeElement) document.activeElement.blur()"); + evaluateJavaScriptSync(view.page(), "window.scrollTo(0, 0)"); + QTRY_COMPARE(getScrollPosition(), 0); +} + +void TouchInputTest::touchTap() +{ + auto singleTap = [target = view.focusProxy()] (const QPoint& tapCoords) -> void { + QTest::touchEvent(target, s_touchDevice).press(1, tapCoords, target); + QTest::touchEvent(target, s_touchDevice).stationary(1); + QTest::touchEvent(target, s_touchDevice).release(1, tapCoords, target); + }; + + // Single tap on text doesn't trigger a selection + singleTap(textCenter); + QTRY_COMPARE(activeElement(), QString()); + QTRY_VERIFY(!view.hasSelection()); + + // Single tap inside the input field focuses it without selecting the text + singleTap(inputCenter); + QTRY_COMPARE(activeElement(), QStringLiteral("input")); + QTRY_VERIFY(!view.hasSelection()); + + // Single tap on the div clears the input field focus + singleTap(notextCenter); + QTRY_COMPARE(activeElement(), QString()); + + // Double tap on text still doesn't trigger a selection + singleTap(textCenter); + singleTap(textCenter); + QTRY_COMPARE(activeElement(), QString()); + QTRY_VERIFY(!view.hasSelection()); + + // Double tap inside the input field focuses it and selects the word under it + singleTap(inputCenter); + singleTap(inputCenter); + QTRY_COMPARE(activeElement(), QStringLiteral("input")); + QTRY_COMPARE(view.selectedText(), QStringLiteral("Company2")); + + // Double tap outside the input field behaves like a single tap: clears its focus and selection + singleTap(notextCenter); + singleTap(notextCenter); + QTRY_COMPARE(activeElement(), QString()); + QTRY_VERIFY(!view.hasSelection()); +} + +void TouchInputTest::touchTapAndHold() +{ + auto tapAndHold = [target = view.focusProxy()] (const QPoint& tapCoords) -> void { + QTest::touchEvent(target, s_touchDevice).press(1, tapCoords, target); + QTest::touchEvent(target, s_touchDevice).stationary(1); + QTest::qWait(1000); + QTest::touchEvent(target, s_touchDevice).release(1, tapCoords, target); + }; + + // Tap-and-hold on text selects the word under it + tapAndHold(textCenter); + QTRY_COMPARE(activeElement(), QString()); + QTRY_COMPARE(view.selectedText(), QStringLiteral("Company")); + + // Tap-and-hold inside the input field focuses it and selects the word under it + tapAndHold(inputCenter); + QTRY_COMPARE(activeElement(), QStringLiteral("input")); + QTRY_COMPARE(view.selectedText(), QStringLiteral("Company2")); + + // Only test the page context menu on Windows, as Linux doesn't handle context menus consistently + // and other non-desktop platforms like Android may not even support context menus at all +#if defined(Q_OS_WIN) + // Tap-and-hold clears the text selection and shows the page's context menu + QVERIFY(QApplication::activePopupWidget() == nullptr); + tapAndHold(notextCenter); + QTRY_COMPARE(activeElement(), QString()); + QTRY_VERIFY(!view.hasSelection()); + QTRY_VERIFY(QApplication::activePopupWidget() != nullptr); + + QApplication::activePopupWidget()->close(); + QVERIFY(QApplication::activePopupWidget() == nullptr); +#endif +} + +void TouchInputTest::touchTapAndHoldCancelled() +{ + auto cancelledTapAndHold = [target = view.focusProxy()] (const QPoint& tapCoords) -> void { + QTest::touchEvent(target, s_touchDevice).press(1, tapCoords, target); + QTest::touchEvent(target, s_touchDevice).stationary(1); + QTest::qWait(1000); + QWindowSystemInterface::handleTouchCancelEvent(target->windowHandle(), s_touchDevice); + }; + + // A cancelled tap-and-hold should cancel text selection, but currently doesn't + cancelledTapAndHold(textCenter); + QEXPECT_FAIL("", "Incorrect Chromium selection behavior when cancelling tap-and-hold on text", Continue); + QTRY_VERIFY_WITH_TIMEOUT(!view.hasSelection(), 100); + + // A cancelled tap-and-hold should cancel input field focusing and selection, but currently doesn't + cancelledTapAndHold(inputCenter); + QEXPECT_FAIL("", "Incorrect Chromium selection behavior when cancelling tap-and-hold on input field", Continue); + QTRY_VERIFY_WITH_TIMEOUT(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString().isEmpty(), 100); + QEXPECT_FAIL("", "Incorrect Chromium focus behavior when cancelling tap-and-hold on input field", Continue); + QTRY_VERIFY_WITH_TIMEOUT(!view.hasSelection(), 100); + + // Only test the page context menu on Windows, as Linux doesn't handle context menus consistently + // and other non-desktop platforms like Android may not even support context menus at all +#if defined(Q_OS_WIN) + // A cancelled tap-and-hold cancels the context menu + QVERIFY(QApplication::activePopupWidget() == nullptr); + cancelledTapAndHold(notextCenter); + QVERIFY(QApplication::activePopupWidget() == nullptr); +#endif +} + +void TouchInputTest::scrolling() +{ + int p = getScrollPosition(); + QCOMPARE(p, 0); + + // scroll a bit down... + for (int i = 0; i < 3; ++i) { + gestureScroll(/* down = */true); + int positionBefore = p; + QTRY_VERIFY2(getScrollPosition(&p) > positionBefore, qPrintable(QString("i: %1, position: %2 -> %3").arg(i).arg(positionBefore).arg(p))); + } + + // ... and then scroll page again but in opposite direction + for (int i = 0; i < 3; ++i) { + gestureScroll(/* down = */false); + int positionBefore = p; + QTRY_VERIFY2(getScrollPosition(&p) < positionBefore, qPrintable(QString("i: %1, position: %2 -> %3").arg(i).arg(positionBefore).arg(p))); + } + + QTRY_COMPARE(getScrollPosition(), 0); +} + +void TouchInputTest::pinchZoom_data() +{ + QTest::addColumn<bool>("tapOneByOne"); + QTest::addRow("sequential") << true; + QTest::addRow("simultaneous") << false; +} + +void TouchInputTest::pinchZoom() +{ + QFETCH(bool, tapOneByOne); + double scale = getScaleFactor(); + QCOMPARE(scale, 1.0); + + for (int i = 0; i < 3; ++i) { + gesturePinch(/* zoomIn = */true, tapOneByOne); + QTRY_VERIFY2(getScaleFactor(&scale) > 1.5, qPrintable(QString("i: %1, scale: %2").arg(i).arg(scale))); + gesturePinch(/* zoomIn = */false, tapOneByOne); + QTRY_COMPARE(getScaleFactor(&scale), 1.0); + } +} + +void TouchInputTest::complexSequence() +{ + auto t = view.focusProxy(); + QPoint pc(view.width() / 2, view.height() / 2), p1 = pc - QPoint(50, 25), p2 = pc + QPoint(50, 25); + + for (int i = 0; i < 4; ++i) { + QTest::touchEvent(t, s_touchDevice).press(42, p1, t); QTest::qWait(50); + QTest::touchEvent(t, s_touchDevice).stationary(42).press(24, p2, t); QTest::qWait(50); + QTest::touchEvent(t, s_touchDevice).release(42, p1, t).release(24, p2, t); + + // for additional variablity add zooming in on even steps and zooming out on odd steps + // MEMO scroll position will always be 0 while viewport scale factor > 1.0, so do zoom in after scroll + bool zoomIn = i % 2 == 0; + + if (!zoomIn) { + gesturePinch(false); + QTRY_COMPARE(getScaleFactor(), 1.0); + } + + int p = getScrollPosition(), positionBefore = p; + gestureScroll(true); + QTRY_VERIFY2_WITH_TIMEOUT(getScrollPosition(&p) > positionBefore, qPrintable(QString("i: %1, position: %2 -> %3").arg(i).arg(positionBefore).arg(p)), 1000); + + if (zoomIn) { + double s = getScaleFactor(), scaleBefore = s; + gesturePinch(true); + QTRY_VERIFY2(getScaleFactor(&s) > scaleBefore, qPrintable(QString("i: %1, scale: %2").arg(i).arg(s))); + } + } +} + +QTEST_MAIN(TouchInputTest) +#include "tst_touchinput.moc" diff --git a/tests/auto/widgets/util.h b/tests/auto/widgets/util.h index e030d1a2f..3be9a91b9 100644 --- a/tests/auto/widgets/util.h +++ b/tests/auto/widgets/util.h @@ -184,6 +184,43 @@ static inline bool loadSync(QWebEngineView *view, const QUrl &url, bool ok = tru return loadSync(view->page(), url, ok); } +static inline QPoint elementCenter(QWebEnginePage *page, const QString &id) +{ + const QString jsCode( + "(function(){" + " var elem = document.getElementById('" + id + "');" + " var rect = elem.getBoundingClientRect();" + " return [(rect.left + rect.right) / 2, (rect.top + rect.bottom) / 2];" + "})()"); + QVariantList rectList = evaluateJavaScriptSync(page, jsCode).toList(); + + if (rectList.count() != 2) { + qWarning("elementCenter failed."); + return QPoint(); + } + + return QPoint(rectList.at(0).toInt(), rectList.at(1).toInt()); +} + +static inline QRect elementGeometry(QWebEnginePage *page, const QString &id) +{ + const QString jsCode( + "(function() {" + " var elem = document.getElementById('" + id + "');" + " var rect = elem.getBoundingClientRect();" + " return [rect.left, rect.top, rect.right, rect.bottom];" + "})()"); + QVariantList coords = evaluateJavaScriptSync(page, jsCode).toList(); + + if (coords.count() != 4) { + qWarning("elementGeometry faield."); + return QRect(); + } + + return QRect(coords[0].toInt(), coords[1].toInt(), coords[2].toInt(), coords[3].toInt()); +} + + #define W_QSKIP(a, b) QSKIP(a) #define W_QTEST_MAIN(TestObject, params) \ diff --git a/tests/auto/widgets/widgets.pro b/tests/auto/widgets/widgets.pro index 947587f5e..4ec9e5d63 100644 --- a/tests/auto/widgets/widgets.pro +++ b/tests/auto/widgets/widgets.pro @@ -22,6 +22,9 @@ SUBDIRS += \ qwebenginesettings \ qwebengineview +# Synthetic touch events are not supported on macOS +!macos: SUBDIRS += touchinput + qtConfig(accessibility) { SUBDIRS += accessibility } |