summaryrefslogtreecommitdiffstats
path: root/tests/auto/widgets/touchinput/tst_touchinput.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'tests/auto/widgets/touchinput/tst_touchinput.cpp')
-rw-r--r--tests/auto/widgets/touchinput/tst_touchinput.cpp372
1 files changed, 372 insertions, 0 deletions
diff --git a/tests/auto/widgets/touchinput/tst_touchinput.cpp b/tests/auto/widgets/touchinput/tst_touchinput.cpp
new file mode 100644
index 000000000..42178558c
--- /dev/null
+++ b/tests/auto/widgets/touchinput/tst_touchinput.cpp
@@ -0,0 +1,372 @@
+// Copyright (C) 2020 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+
+#include <util.h>
+
+#include <QtGui/qpa/qwindowsysteminterface.h>
+#include <QSignalSpy>
+#include <QTest>
+#include <QPointingDevice>
+#include <QWebEngineSettings>
+#include <QWebEngineView>
+
+static QPointingDevice* s_touchDevice = nullptr;
+
+struct Page : QWebEnginePage
+{
+ QStringList alerts;
+ void javaScriptAlert(const QUrl &/*origin*/, const QString &msg) override { alerts.append(msg); }
+};
+
+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();
+ void buttonClickHandler();
+ void htmlSelectPopup();
+
+private:
+ Page page;
+ QWebEngineView view;
+ QSignalSpy loadSpy { &view, &QWebEngineView::loadFinished };
+ QPoint notextCenter, textCenter, inputCenter;
+
+ QString activeElement() { return evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString(); }
+
+ void makeTouch(QWindow *w, const QPoint &p) {
+ QTest::touchEvent(w, s_touchDevice).press(1, p);
+ QTest::touchEvent(w, s_touchDevice).release(1, p);
+ }
+ void makeTouch(const QPoint &p) { makeTouch(view.windowHandle(), p); }
+
+ 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(1, p, target);
+
+ QSignalSpy spy(view.page(), &QWebEnginePage::scrollPositionChanged);
+ 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(1, p, target);
+ spy.wait();
+ }
+
+ QTest::touchEvent(target, s_touchDevice).release(1, 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(0, t1, target);
+ QTest::touchEvent(target, s_touchDevice).stationary(0).press(1, t2, target);
+ } else {
+ QTest::touchEvent(target, s_touchDevice).press(0, t1, target).press(1, 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(1, t1, target).move(0, t2, target);
+ }
+
+ if (tapOneByOne) {
+ QTest::touchEvent(target, s_touchDevice).stationary(0).release(1, t2, target);
+ QTest::touchEvent(target, s_touchDevice).release(0, t1, target);
+ } else {
+ QTest::touchEvent(target, s_touchDevice).release(0, t1, target).release(1, t2, target);
+ }
+ }
+
+ int getScrollPosition(int *position = nullptr) {
+ int p = evaluateJavaScriptSync(view.page(), "window.scrollY").toInt();
+ return position ? (*position = p) : p;
+ }
+
+ int pageScrollPosition() {
+ // this one is updated later in page in asynchronous way
+ return qRound(view.page()->scrollPosition().y());
+ }
+
+ 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.setPage(&page);
+ 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' style='width: 150px;' type='text' value='The Qt Company2' /></form>"
+ "<button id='btn' type='button' onclick='alert(\"button clicked!\")'>Click Me!</button>"
+ "<select id='select' onchange='alert(\"option changed to: \" + this.value)'>"
+ "<option value='O1'>O1</option><option value='O2'>O2</option><option value='O3'>O3</option></select>"
+ "<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);
+ QTRY_COMPARE(pageScrollPosition(), 0);
+ page.alerts.clear();
+}
+
+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.0, 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)));
+ }
+ }
+}
+
+void TouchInputTest::buttonClickHandler()
+{
+ auto buttonCenter = elementGeometry(view.page(), "btn").center();
+ makeTouch(buttonCenter);
+ QTRY_VERIFY(!page.alerts.isEmpty());
+ QCOMPARE(page.alerts.first(), "button clicked!");
+ QCOMPARE(page.alerts.size(), 1);
+ QEXPECT_FAIL("", "Shouldn't trigger twice due to synthesized mouse events for touch", Continue);
+ QTRY_VERIFY_WITH_TIMEOUT(page.alerts.size() == 2, 500);
+}
+
+void TouchInputTest::htmlSelectPopup()
+{
+ auto selectRect = elementGeometry(view.page(), "select");
+ makeTouch(selectRect.center());
+ QTRY_VERIFY(QApplication::activePopupWidget());
+ QCOMPARE(activeElement(), QStringLiteral("select"));
+
+ auto popup = QApplication::activePopupWidget();
+ makeTouch(popup->windowHandle(), QPoint(popup->width() / 2, popup->height() / 2));
+ QTRY_VERIFY(!QApplication::activePopupWidget());
+
+ QTRY_VERIFY(!page.alerts.isEmpty());
+ QCOMPARE(page.alerts.first(), "option changed to: O2");
+ QEXPECT_FAIL("", "Shouldn't trigger twice due to synthesized mouse events for touch", Continue);
+ QTRY_VERIFY_WITH_TIMEOUT(page.alerts.size() == 2, 500);
+}
+
+QTEST_MAIN(TouchInputTest)
+#include "tst_touchinput.moc"