blob: 5e16361c5e4dbc6266bbb696cbbca6d0add2b246 [file] [log] [blame]
/*
Copyright (C) 2016 The Qt Company Ltd.
Copyright (C) 2009 Torch Mobile Inc.
Copyright (C) 2009 Girish Ramakrishnan <girish@forwardbias.in>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public License
along with this library; see the file COPYING.LIB. If not, write to
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
*/
#include <qtest.h>
#include "../util.h"
#include <private/qinputmethod_p.h>
#include <qpainter.h>
#include <qpagelayout.h>
#include <qpa/qplatforminputcontext.h>
#include <qwebengineview.h>
#include <qwebenginepage.h>
#include <qwebenginesettings.h>
#include <qnetworkrequest.h>
#include <qdiriterator.h>
#include <qstackedlayout.h>
#include <qtemporarydir.h>
#include <QClipboard>
#include <QCompleter>
#include <QLabel>
#include <QLineEdit>
#include <QHBoxLayout>
#include <QMenu>
#include <QQuickItem>
#include <QQuickWidget>
#include <QtWebEngineCore/qwebenginehttprequest.h>
#include <QTcpServer>
#include <QTcpSocket>
#include <QStyle>
#include <QtWidgets/qaction.h>
#include <QWebEngineProfile>
#include <QtCore/qregularexpression.h>
#define VERIFY_INPUTMETHOD_HINTS(actual, expect) \
QVERIFY(actual == (expect | Qt::ImhNoPredictiveText | Qt::ImhNoTextHandles | Qt::ImhNoEditMenu));
#define QTRY_COMPARE_WITH_TIMEOUT_FAIL_BLOCK(__expr, __expected, __timeout, __fail_block) \
do { \
QTRY_IMPL(((__expr) == (__expected)), __timeout);\
if (__expr != __expected)\
__fail_block\
QCOMPARE((__expr), __expected); \
} while (0)
static QTouchDevice* 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();
static void mouseEvent(QEvent::Type type, QWidget *widget, const QPoint &pos)
{
QTest::qWait(QTest::defaultMouseDelay());
lastMouseTimestamp += QTest::defaultMouseDelay();
QMouseEvent me(type, pos, Qt::LeftButton, Qt::LeftButton, Qt::NoModifier);
me.setTimestamp(++lastMouseTimestamp);
QSpontaneKeyEvent::setSpontaneous(&me);
qApp->sendEvent(widget, &me);
}
static void mouseMultiClick(QWidget *widget, const QPoint pos, int clickCount)
{
for (int i = 0; i < clickCount; ++i) {
mouseEvent(QMouseEvent::MouseButtonPress, widget, pos);
mouseEvent(QMouseEvent::MouseButtonRelease, widget, pos);
}
lastMouseTimestamp += mouseDoubleClickInterval;
}
}
QT_END_NAMESPACE
class tst_QWebEngineView : public QObject
{
Q_OBJECT
public Q_SLOTS:
void initTestCase();
void cleanupTestCase();
void init();
void cleanup();
private Q_SLOTS:
void renderingAfterMaxAndBack();
void renderHints();
void getWebKitVersion();
void changePage_data();
void changePage();
void reusePage_data();
void reusePage();
void microFocusCoordinates();
void focusInputTypes();
void unhandledKeyEventPropagation();
void horizontalScrollbarTest();
void crashTests();
#if !(defined(WTF_USE_QT_MOBILE_THEME) && WTF_USE_QT_MOBILE_THEME)
void setPalette_data();
void setPalette();
#endif
void doNotSendMouseKeyboardEventsWhenDisabled();
void doNotSendMouseKeyboardEventsWhenDisabled_data();
void stopSettingFocusWhenDisabled();
void stopSettingFocusWhenDisabled_data();
void focusOnNavigation_data();
void focusOnNavigation();
void focusInternalRenderWidgetHostViewQuickItem();
void doNotBreakLayout();
void changeLocale();
void inputMethodsTextFormat_data();
void inputMethodsTextFormat();
void keyboardEvents();
void keyboardFocusAfterPopup();
void mouseClick();
void touchTap();
void touchTapAndHold();
void touchTapAndHoldCancelled();
void postData();
void inputFieldOverridesShortcuts();
void softwareInputPanel();
void inputContextQueryInput();
void inputMethods();
void textSelectionInInputField();
void textSelectionOutOfInputField();
void hiddenText();
void emptyInputMethodEvent();
void imeComposition();
void imeCompositionQueryEvent_data();
void imeCompositionQueryEvent();
void newlineInTextarea();
void imeJSInputEvents();
void mouseLeave();
#ifndef QT_NO_CLIPBOARD
void globalMouseSelection();
#endif
void noContextMenu();
void contextMenu_data();
void contextMenu();
void webUIURLs_data();
void webUIURLs();
void visibilityState();
void visibilityState2();
void visibilityState3();
void jsKeyboardEvent_data();
void jsKeyboardEvent();
void deletePage();
void closeOpenerTab();
void switchPage();
void setPageDeletesImplicitPage();
void setPageDeletesImplicitPage2();
void setViewDeletesImplicitPage();
void setPagePreservesExplicitPage();
void setViewPreservesExplicitPage();
void closeDiscardsPage();
};
// This will be called before the first test function is executed.
// It is only called once.
void tst_QWebEngineView::initTestCase()
{
s_touchDevice = QTest::createTouchDevice();
}
// This will be called after the last test function is executed.
// It is only called once.
void tst_QWebEngineView::cleanupTestCase()
{
}
// This will be called before each test function is executed.
void tst_QWebEngineView::init()
{
}
// This will be called after every test function.
void tst_QWebEngineView::cleanup()
{
}
void tst_QWebEngineView::renderHints()
{
#if !defined(QWEBENGINEVIEW_RENDERHINTS)
QSKIP("QWEBENGINEVIEW_RENDERHINTS");
#else
QWebEngineView webView;
// default is only text antialiasing + smooth pixmap transform
QVERIFY(!(webView.renderHints() & QPainter::Antialiasing));
QVERIFY(webView.renderHints() & QPainter::TextAntialiasing);
QVERIFY(webView.renderHints() & QPainter::SmoothPixmapTransform);
#if QT_DEPRECATED_SINCE(5, 14)
QVERIFY(!(webView.renderHints() & QPainter::HighQualityAntialiasing));
#endif
QVERIFY(!(webView.renderHints() & QPainter::Antialiasing));
webView.setRenderHint(QPainter::Antialiasing, true);
QVERIFY(webView.renderHints() & QPainter::Antialiasing);
QVERIFY(webView.renderHints() & QPainter::TextAntialiasing);
QVERIFY(webView.renderHints() & QPainter::SmoothPixmapTransform);
#if QT_DEPRECATED_SINCE(5, 14)
QVERIFY(!(webView.renderHints() & QPainter::HighQualityAntialiasing));
#endif
QVERIFY(!(webView.renderHints() & QPainter::Antialiasing));
webView.setRenderHint(QPainter::Antialiasing, false);
QVERIFY(!(webView.renderHints() & QPainter::Antialiasing));
QVERIFY(webView.renderHints() & QPainter::TextAntialiasing);
QVERIFY(webView.renderHints() & QPainter::SmoothPixmapTransform);
#if QT_DEPRECATED_SINCE(5, 14)
QVERIFY(!(webView.renderHints() & QPainter::HighQualityAntialiasing));
#endif
QVERIFY(!(webView.renderHints() & QPainter::Antialiasing));
webView.setRenderHint(QPainter::SmoothPixmapTransform, true);
QVERIFY(!(webView.renderHints() & QPainter::Antialiasing));
QVERIFY(webView.renderHints() & QPainter::TextAntialiasing);
QVERIFY(webView.renderHints() & QPainter::SmoothPixmapTransform);
#if QT_DEPRECATED_SINCE(5, 14)
QVERIFY(!(webView.renderHints() & QPainter::HighQualityAntialiasing));
#endif
QVERIFY(!(webView.renderHints() & QPainter::Antialiasing));
webView.setRenderHint(QPainter::SmoothPixmapTransform, false);
QVERIFY(webView.renderHints() & QPainter::TextAntialiasing);
QVERIFY(!(webView.renderHints() & QPainter::SmoothPixmapTransform));
#if QT_DEPRECATED_SINCE(5, 14)
QVERIFY(!(webView.renderHints() & QPainter::HighQualityAntialiasing));
#endif
QVERIFY(!(webView.renderHints() & QPainter::Antialiasing));
#endif
}
void tst_QWebEngineView::getWebKitVersion()
{
#if !defined(QWEBENGINEVERSION)
QSKIP("QWEBENGINEVERSION");
#else
QVERIFY(qWebKitVersion().toDouble() > 0);
#endif
}
void tst_QWebEngineView::changePage_data()
{
QString html = "<html><head><title>%1</title>"
"<link rel='icon' href='qrc:///resources/image2.png'></head></html>";
QUrl urlFrom("data:text/html," + html.arg("TitleFrom"));
QUrl urlTo("data:text/html," + html.arg("TitleTo"));
QUrl nullPage("data:text/html,<html/>");
QTest::addColumn<QUrl>("urlFrom");
QTest::addColumn<QUrl>("urlTo");
QTest::addColumn<bool>("fromIsNullPage");
QTest::addColumn<bool>("toIsNullPage");
QTest::newRow("From empty page to url") << nullPage << urlTo << true << false;
QTest::newRow("From url to empty content page") << urlFrom << nullPage << false << true;
QTest::newRow("From one content to another") << urlFrom << urlTo << false << false;
}
void tst_QWebEngineView::changePage()
{
QScopedPointer<QWebEngineView> view(new QWebEngineView); view->resize(640, 480); view->show();
QFETCH(QUrl, urlFrom);
QFETCH(QUrl, urlTo);
QFETCH(bool, fromIsNullPage);
QFETCH(bool, toIsNullPage);
QSignalSpy spyUrl(view.get(), &QWebEngineView::urlChanged);
QSignalSpy spyTitle(view.get(), &QWebEngineView::titleChanged);
QSignalSpy spyIconUrl(view.get(), &QWebEngineView::iconUrlChanged);
QSignalSpy spyIcon(view.get(), &QWebEngineView::iconChanged);
QScopedPointer<QWebEnginePage> pageFrom(new QWebEnginePage);
QSignalSpy pageFromLoadSpy(pageFrom.get(), &QWebEnginePage::loadFinished);
QSignalSpy pageFromIconLoadSpy(pageFrom.get(), &QWebEnginePage::iconChanged);
pageFrom->load(urlFrom);
QTRY_COMPARE(pageFromLoadSpy.count(), 1);
QCOMPARE(pageFromLoadSpy.last().value(0).toBool(), true);
if (!fromIsNullPage) {
QTRY_COMPARE(pageFromIconLoadSpy.count(), 1);
QVERIFY(!pageFromIconLoadSpy.last().value(0).isNull());
}
view->setPage(pageFrom.get());
QTRY_COMPARE(spyUrl.count(), 1);
QCOMPARE(spyUrl.last().value(0).toUrl(), pageFrom->url());
QTRY_COMPARE(spyTitle.count(), 1);
QCOMPARE(spyTitle.last().value(0).toString(), pageFrom->title());
QTRY_COMPARE(spyIconUrl.count(), fromIsNullPage ? 0 : 1);
QTRY_COMPARE(spyIcon.count(), fromIsNullPage ? 0 : 1);
if (!fromIsNullPage) {
QVERIFY(!pageFrom->iconUrl().isEmpty());
QCOMPARE(spyIconUrl.last().value(0).toUrl(), pageFrom->iconUrl());
QCOMPARE(spyIcon.last().value(0), QVariant::fromValue(pageFrom->icon()));
}
QScopedPointer<QWebEnginePage> pageTo(new QWebEnginePage);
QSignalSpy pageToLoadSpy(pageTo.get(), &QWebEnginePage::loadFinished);
QSignalSpy pageToIconLoadSpy(pageTo.get(), &QWebEnginePage::iconChanged);
pageTo->load(urlTo);
QTRY_COMPARE(pageToLoadSpy.count(), 1);
QCOMPARE(pageToLoadSpy.last().value(0).toBool(), true);
if (!toIsNullPage) {
QTRY_COMPARE(pageToIconLoadSpy.count(), 1);
QVERIFY(!pageToIconLoadSpy.last().value(0).isNull());
}
view->setPage(pageTo.get());
QTRY_COMPARE(spyUrl.count(), 2);
QCOMPARE(spyUrl.last().value(0).toUrl(), pageTo->url());
QTRY_COMPARE(spyTitle.count(), 2);
QCOMPARE(spyTitle.last().value(0).toString(), pageTo->title());
bool iconIsSame = fromIsNullPage == toIsNullPage;
int iconChangeNotifyCount = fromIsNullPage ? (iconIsSame ? 0 : 1) : (iconIsSame ? 1 : 2);
QTRY_COMPARE(spyIconUrl.count(), iconChangeNotifyCount);
QTRY_COMPARE(spyIcon.count(), iconChangeNotifyCount);
QCOMPARE(pageFrom->iconUrl() == pageTo->iconUrl(), iconIsSame);
if (!iconIsSame) {
QCOMPARE(spyIconUrl.last().value(0).toUrl(), pageTo->iconUrl());
QCOMPARE(spyIcon.last().value(0), QVariant::fromValue(pageTo->icon()));
}
// verify no emits on destroy with the same number of signals in spy
view.reset();
qApp->processEvents();
QTRY_COMPARE(spyUrl.count(), 2);
QTRY_COMPARE(spyTitle.count(), 2);
QTRY_COMPARE(spyIconUrl.count(), iconChangeNotifyCount);
QTRY_COMPARE(spyIcon.count(), iconChangeNotifyCount);
}
void tst_QWebEngineView::reusePage_data()
{
QTest::addColumn<QString>("html");
QTest::newRow("WithoutPlugin") << "<html><body id='b'>text</body></html>";
QTest::newRow("WindowedPlugin") << QString("<html><body id='b'>text<embed src='resources/test.swf'></embed></body></html>");
QTest::newRow("WindowlessPlugin") << QString("<html><body id='b'>text<embed src='resources/test.swf' wmode=\"transparent\"></embed></body></html>");
}
void tst_QWebEngineView::reusePage()
{
if (!QDir(TESTS_SOURCE_DIR).exists())
W_QSKIP(QString("This test requires access to resources found in '%1'").arg(TESTS_SOURCE_DIR).toLatin1().constData(), SkipAll);
QDir::setCurrent(TESTS_SOURCE_DIR);
QFETCH(QString, html);
QWebEngineView* view1 = new QWebEngineView;
QPointer<QWebEnginePage> page = new QWebEnginePage;
view1->setPage(page.data());
page.data()->settings()->setAttribute(QWebEngineSettings::PluginsEnabled, true);
page->setHtml(html, QUrl::fromLocalFile(TESTS_SOURCE_DIR));
if (html.contains("</embed>")) {
// some reasonable time for the PluginStream to feed test.swf to flash and start painting
QSignalSpy spyFinished(view1, &QWebEngineView::loadFinished);
QVERIFY(spyFinished.wait(2000));
}
view1->show();
QVERIFY(QTest::qWaitForWindowExposed(view1));
delete view1;
QVERIFY(page != 0); // deleting view must not have deleted the page, since it's not a child of view
QWebEngineView *view2 = new QWebEngineView;
view2->setPage(page.data());
view2->show(); // in Windowless mode, you should still be able to see the plugin here
QVERIFY(QTest::qWaitForWindowExposed(view2));
delete view2;
delete page.data(); // must not crash
QDir::setCurrent(QApplication::applicationDirPath());
}
// Class used in crashTests
class WebViewCrashTest : public QObject {
Q_OBJECT
QWebEngineView* m_view;
public:
bool m_invokedStop;
bool m_stopBypassed;
WebViewCrashTest(QWebEngineView* view)
: m_view(view)
, m_invokedStop(false)
, m_stopBypassed(false)
{
view->connect(view, SIGNAL(loadProgress(int)), this, SLOT(loading(int)));
}
private Q_SLOTS:
void loading(int progress)
{
qDebug() << "progress: " << progress;
if (progress > 0 && progress < 100) {
QVERIFY(!m_invokedStop);
m_view->stop();
m_invokedStop = true;
} else if (!m_invokedStop && progress == 100) {
m_stopBypassed = true;
}
}
};
// Should not crash.
void tst_QWebEngineView::crashTests()
{
// Test if loading can be stopped in loadProgress handler without crash.
// Test page should have frames.
QWebEngineView view;
WebViewCrashTest tester(&view);
QUrl url("qrc:///resources/index.html");
view.load(url);
// If the verification fails, it means that either stopping doesn't work, or the hardware is
// too slow to load the page and thus to slow to issue the first loadProgress > 0 signal.
QTRY_VERIFY_WITH_TIMEOUT(tester.m_invokedStop || tester.m_stopBypassed, 10000);
if (tester.m_stopBypassed)
QEXPECT_FAIL("", "Loading was too fast to stop", Continue);
QVERIFY(tester.m_invokedStop);
}
void tst_QWebEngineView::microFocusCoordinates()
{
QWebEngineView webView;
webView.resize(640, 480);
webView.show();
QVERIFY(QTest::qWaitForWindowExposed(&webView));
QSignalSpy scrollSpy(webView.page(), SIGNAL(scrollPositionChanged(QPointF)));
QSignalSpy loadFinishedSpy(&webView, SIGNAL(loadFinished(bool)));
webView.page()->setHtml("<html><body>"
"<input type='text' id='input1' value='' maxlength='20'/><br>"
"<canvas id='canvas1' width='500' height='500'></canvas>"
"<input type='password'/><br>"
"<canvas id='canvas2' width='500' height='500'></canvas>"
"</body></html>");
QVERIFY(loadFinishedSpy.wait());
evaluateJavaScriptSync(webView.page(), "document.getElementById('input1').focus()");
QTRY_COMPARE(evaluateJavaScriptSync(webView.page(), "document.activeElement.id").toString(), QStringLiteral("input1"));
QTRY_VERIFY(webView.focusProxy()->inputMethodQuery(Qt::ImCursorRectangle).isValid());
QVariant initialMicroFocus = webView.focusProxy()->inputMethodQuery(Qt::ImCursorRectangle);
evaluateJavaScriptSync(webView.page(), "window.scrollBy(0, 50)");
QTRY_VERIFY(scrollSpy.count() > 0);
QTRY_VERIFY(webView.focusProxy()->inputMethodQuery(Qt::ImCursorRectangle).isValid());
QVariant currentMicroFocus = webView.focusProxy()->inputMethodQuery(Qt::ImCursorRectangle);
QCOMPARE(initialMicroFocus.toRect().translated(QPoint(0,-50)), currentMicroFocus.toRect());
}
void tst_QWebEngineView::focusInputTypes()
{
const QPlatformInputContext *context = QGuiApplicationPrivate::platformIntegration()->inputContext();
bool imeHasHiddenTextCapability = context && context->hasCapability(QPlatformInputContext::HiddenTextCapability);
QWebEngineView webView;
webView.resize(640, 480);
webView.show();
QVERIFY(QTest::qWaitForWindowExposed(&webView));
QSignalSpy loadFinishedSpy(&webView, SIGNAL(loadFinished(bool)));
webView.load(QUrl("qrc:///resources/input_types.html"));
QVERIFY(loadFinishedSpy.wait());
auto inputMethodQuery = [&webView](Qt::InputMethodQuery query) {
QInputMethodQueryEvent event(query);
QApplication::sendEvent(webView.focusProxy(), &event);
return event.value(query);
};
// 'text' field
QPoint textInputCenter = elementCenter(webView.page(), "textInput");
QTest::mouseClick(webView.focusProxy(), Qt::LeftButton, {}, textInputCenter);
QTRY_COMPARE(evaluateJavaScriptSync(webView.page(), "document.activeElement.id").toString(), QStringLiteral("textInput"));
VERIFY_INPUTMETHOD_HINTS(webView.focusProxy()->inputMethodHints(), Qt::ImhPreferLowercase);
QVERIFY(webView.focusProxy()->testAttribute(Qt::WA_InputMethodEnabled));
QTRY_VERIFY(inputMethodQuery(Qt::ImEnabled).toBool());
// 'password' field
QPoint passwordInputCenter = elementCenter(webView.page(), "passwordInput");
QTest::mouseClick(webView.focusProxy(), Qt::LeftButton, {}, passwordInputCenter);
QTRY_COMPARE(evaluateJavaScriptSync(webView.page(), "document.activeElement.id").toString(), QStringLiteral("passwordInput"));
VERIFY_INPUTMETHOD_HINTS(webView.focusProxy()->inputMethodHints(), (Qt::ImhSensitiveData | Qt::ImhNoPredictiveText | Qt::ImhNoAutoUppercase | Qt::ImhHiddenText));
QVERIFY(!webView.focusProxy()->testAttribute(Qt::WA_InputMethodEnabled));
QTRY_COMPARE(inputMethodQuery(Qt::ImEnabled).toBool(), imeHasHiddenTextCapability);
// 'tel' field
QPoint telInputCenter = elementCenter(webView.page(), "telInput");
QTest::mouseClick(webView.focusProxy(), Qt::LeftButton, {}, telInputCenter);
QTRY_COMPARE(evaluateJavaScriptSync(webView.page(), "document.activeElement.id").toString(), QStringLiteral("telInput"));
VERIFY_INPUTMETHOD_HINTS(webView.focusProxy()->inputMethodHints(), Qt::ImhDialableCharactersOnly);
QVERIFY(webView.focusProxy()->testAttribute(Qt::WA_InputMethodEnabled));
QTRY_VERIFY(inputMethodQuery(Qt::ImEnabled).toBool());
// 'number' field
QPoint numberInputCenter = elementCenter(webView.page(), "numberInput");
QTest::mouseClick(webView.focusProxy(), Qt::LeftButton, {}, numberInputCenter);
QTRY_COMPARE(evaluateJavaScriptSync(webView.page(), "document.activeElement.id").toString(), QStringLiteral("numberInput"));
VERIFY_INPUTMETHOD_HINTS(webView.focusProxy()->inputMethodHints(), Qt::ImhFormattedNumbersOnly);
QVERIFY(webView.focusProxy()->testAttribute(Qt::WA_InputMethodEnabled));
QTRY_VERIFY(inputMethodQuery(Qt::ImEnabled).toBool());
// 'email' field
QPoint emailInputCenter = elementCenter(webView.page(), "emailInput");
QTest::mouseClick(webView.focusProxy(), Qt::LeftButton, {}, emailInputCenter);
QTRY_COMPARE(evaluateJavaScriptSync(webView.page(), "document.activeElement.id").toString(), QStringLiteral("emailInput"));
VERIFY_INPUTMETHOD_HINTS(webView.focusProxy()->inputMethodHints(), Qt::ImhEmailCharactersOnly);
QVERIFY(webView.focusProxy()->testAttribute(Qt::WA_InputMethodEnabled));
QTRY_VERIFY(inputMethodQuery(Qt::ImEnabled).toBool());
// 'url' field
QPoint urlInputCenter = elementCenter(webView.page(), "urlInput");
QTest::mouseClick(webView.focusProxy(), Qt::LeftButton, {}, urlInputCenter);
QTRY_COMPARE(evaluateJavaScriptSync(webView.page(), "document.activeElement.id").toString(), QStringLiteral("urlInput"));
VERIFY_INPUTMETHOD_HINTS(webView.focusProxy()->inputMethodHints(), (Qt::ImhUrlCharactersOnly | Qt::ImhNoPredictiveText | Qt::ImhNoAutoUppercase));
QVERIFY(webView.focusProxy()->testAttribute(Qt::WA_InputMethodEnabled));
QTRY_VERIFY(inputMethodQuery(Qt::ImEnabled).toBool());
// 'password' field
QTest::mouseClick(webView.focusProxy(), Qt::LeftButton, {}, passwordInputCenter);
QTRY_COMPARE(evaluateJavaScriptSync(webView.page(), "document.activeElement.id").toString(), QStringLiteral("passwordInput"));
VERIFY_INPUTMETHOD_HINTS(webView.focusProxy()->inputMethodHints(), (Qt::ImhSensitiveData | Qt::ImhNoPredictiveText | Qt::ImhNoAutoUppercase | Qt::ImhHiddenText));
QVERIFY(!webView.focusProxy()->testAttribute(Qt::WA_InputMethodEnabled));
QTRY_COMPARE(inputMethodQuery(Qt::ImEnabled).toBool(), imeHasHiddenTextCapability);
// 'text' type
QTest::mouseClick(webView.focusProxy(), Qt::LeftButton, {}, textInputCenter);
QTRY_COMPARE(evaluateJavaScriptSync(webView.page(), "document.activeElement.id").toString(), QStringLiteral("textInput"));
VERIFY_INPUTMETHOD_HINTS(webView.focusProxy()->inputMethodHints(), Qt::ImhPreferLowercase);
QVERIFY(webView.focusProxy()->testAttribute(Qt::WA_InputMethodEnabled));
QTRY_VERIFY(inputMethodQuery(Qt::ImEnabled).toBool());
// 'password' field
QTest::mouseClick(webView.focusProxy(), Qt::LeftButton, {}, passwordInputCenter);
QTRY_COMPARE(evaluateJavaScriptSync(webView.page(), "document.activeElement.id").toString(), QStringLiteral("passwordInput"));
VERIFY_INPUTMETHOD_HINTS(webView.focusProxy()->inputMethodHints(), (Qt::ImhSensitiveData | Qt::ImhNoPredictiveText | Qt::ImhNoAutoUppercase | Qt::ImhHiddenText));
QVERIFY(!webView.focusProxy()->testAttribute(Qt::WA_InputMethodEnabled));
QTRY_COMPARE(inputMethodQuery(Qt::ImEnabled).toBool(), imeHasHiddenTextCapability);
// 'text area' field
QPoint textAreaCenter = elementCenter(webView.page(), "textArea");
QTest::mouseClick(webView.focusProxy(), Qt::LeftButton, {}, textAreaCenter);
QTRY_COMPARE(evaluateJavaScriptSync(webView.page(), "document.activeElement.id").toString(), QStringLiteral("textArea"));
VERIFY_INPUTMETHOD_HINTS(webView.focusProxy()->inputMethodHints(), (Qt::ImhMultiLine | Qt::ImhPreferLowercase));
QVERIFY(webView.focusProxy()->testAttribute(Qt::WA_InputMethodEnabled));
QTRY_VERIFY(inputMethodQuery(Qt::ImEnabled).toBool());
}
class KeyEventRecordingWidget : public QWidget {
public:
QList<QKeyEvent> pressEvents;
QList<QKeyEvent> releaseEvents;
void keyPressEvent(QKeyEvent *e) override { pressEvents << *e; }
void keyReleaseEvent(QKeyEvent *e) override { releaseEvents << *e; }
};
void tst_QWebEngineView::unhandledKeyEventPropagation()
{
KeyEventRecordingWidget parentWidget;
QWebEngineView webView(&parentWidget);
webView.resize(640, 480);
parentWidget.show();
QVERIFY(QTest::qWaitForWindowExposed(&webView));
QSignalSpy loadFinishedSpy(&webView, SIGNAL(loadFinished(bool)));
webView.load(QUrl("qrc:///resources/keyboardEvents.html"));
QVERIFY(loadFinishedSpy.wait());
evaluateJavaScriptSync(webView.page(), "document.getElementById('first_div').focus()");
QTRY_COMPARE(evaluateJavaScriptSync(webView.page(), "document.activeElement.id").toString(), QStringLiteral("first_div"));
QTest::sendKeyEvent(QTest::Press, webView.focusProxy(), Qt::Key_Right, QString(), Qt::NoModifier);
QTest::sendKeyEvent(QTest::Release, webView.focusProxy(), Qt::Key_Right, QString(), Qt::NoModifier);
// Right arrow key is unhandled thus focus is not changed
QTRY_COMPARE(parentWidget.releaseEvents.size(), 1);
QCOMPARE(evaluateJavaScriptSync(webView.page(), "document.activeElement.id").toString(), QStringLiteral("first_div"));
QTest::sendKeyEvent(QTest::Press, webView.focusProxy(), Qt::Key_Tab, QString(), Qt::NoModifier);
QTest::sendKeyEvent(QTest::Release, webView.focusProxy(), Qt::Key_Tab, QString(), Qt::NoModifier);
// Tab key is handled thus focus is changed
QTRY_COMPARE(parentWidget.releaseEvents.size(), 2);
QCOMPARE(evaluateJavaScriptSync(webView.page(), "document.activeElement.id").toString(), QStringLiteral("second_div"));
QTest::sendKeyEvent(QTest::Press, webView.focusProxy(), Qt::Key_Left, QString(), Qt::NoModifier);
QTest::sendKeyEvent(QTest::Release, webView.focusProxy(), Qt::Key_Left, QString(), Qt::NoModifier);
// Left arrow key is unhandled thus focus is not changed
QTRY_COMPARE(parentWidget.releaseEvents.size(), 3);
QCOMPARE(evaluateJavaScriptSync(webView.page(), "document.activeElement.id").toString(), QStringLiteral("second_div"));
// Focus the button and press 'y'.
evaluateJavaScriptSync(webView.page(), "document.getElementById('submit_button').focus()");
QTRY_COMPARE(evaluateJavaScriptSync(webView.page(), "document.activeElement.id").toString(), QStringLiteral("submit_button"));
QTest::sendKeyEvent(QTest::Press, webView.focusProxy(), Qt::Key_Y, 'y', Qt::NoModifier);
QTest::sendKeyEvent(QTest::Release, webView.focusProxy(), Qt::Key_Y, 'y', Qt::NoModifier);
QTRY_COMPARE(parentWidget.releaseEvents.size(), 4);
// The page will consume the Tab key to change focus between elements while the arrow
// keys won't be used.
QCOMPARE(parentWidget.pressEvents.size(), 3);
QCOMPARE(parentWidget.pressEvents[0].key(), (int)Qt::Key_Right);
QCOMPARE(parentWidget.pressEvents[1].key(), (int)Qt::Key_Left);
QCOMPARE(parentWidget.pressEvents[2].key(), (int)Qt::Key_Y);
// Key releases will all come back unconsumed.
QCOMPARE(parentWidget.releaseEvents[0].key(), (int)Qt::Key_Right);
QCOMPARE(parentWidget.releaseEvents[1].key(), (int)Qt::Key_Tab);
QCOMPARE(parentWidget.releaseEvents[2].key(), (int)Qt::Key_Left);
QCOMPARE(parentWidget.releaseEvents[3].key(), (int)Qt::Key_Y);
}
void tst_QWebEngineView::horizontalScrollbarTest()
{
QString html("<html><body>"
"<div style='width: 1000px; height: 1000px; background-color: green' />"
"</body></html>");
QWebEngineView view;
view.setFixedSize(600, 600);
view.show();
QVERIFY(QTest::qWaitForWindowExposed(&view));
QSignalSpy loadSpy(view.page(), SIGNAL(loadFinished(bool)));
view.setHtml(html);
QTRY_COMPARE(loadSpy.count(), 1);
QVERIFY(view.page()->scrollPosition() == QPoint(0, 0));
QSignalSpy scrollSpy(view.page(), SIGNAL(scrollPositionChanged(QPointF)));
// Note: The test below assumes that the layout direction is Qt::LeftToRight.
QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, QPoint(550, 595));
scrollSpy.wait();
QVERIFY(view.page()->scrollPosition().x() > 0);
// Note: The test below assumes that the layout direction is Qt::LeftToRight.
QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, QPoint(20, 595));
scrollSpy.wait();
QVERIFY(view.page()->scrollPosition() == QPoint(0, 0));
}
#if !(defined(WTF_USE_QT_MOBILE_THEME) && WTF_USE_QT_MOBILE_THEME)
void tst_QWebEngineView::setPalette_data()
{
QTest::addColumn<bool>("active");
QTest::addColumn<bool>("background");
QTest::newRow("activeBG") << true << true;
QTest::newRow("activeFG") << true << false;
QTest::newRow("inactiveBG") << false << true;
QTest::newRow("inactiveFG") << false << false;
}
// Render a QWebEngineView to a QImage twice, each time with a different palette set,
// verify that images rendered are not the same, confirming WebCore usage of
// custom palette on selections.
void tst_QWebEngineView::setPalette()
{
#if !defined(QWEBCONTENTVIEW_SETPALETTE)
QSKIP("QWEBCONTENTVIEW_SETPALETTE");
#else
QString html = "<html><head></head>"
"<body>"
"Some text here"
"</body>"
"</html>";
QFETCH(bool, active);
QFETCH(bool, background);
QWidget* activeView = 0;
// Use controlView to manage active/inactive state of test views by raising
// or lowering their position in the window stack.
QWebEngineView controlView;
controlView.setHtml(html);
QWebEngineView view1;
QPalette palette1;
QBrush brush1(Qt::red);
brush1.setStyle(Qt::SolidPattern);
if (active && background) {
// Rendered image must have red background on an active QWebEngineView.
palette1.setBrush(QPalette::Active, QPalette::Highlight, brush1);
} else if (active && !background) {
// Rendered image must have red foreground on an active QWebEngineView.
palette1.setBrush(QPalette::Active, QPalette::HighlightedText, brush1);
} else if (!active && background) {
// Rendered image must have red background on an inactive QWebEngineView.
palette1.setBrush(QPalette::Inactive, QPalette::Highlight, brush1);
} else if (!active && !background) {
// Rendered image must have red foreground on an inactive QWebEngineView.
palette1.setBrush(QPalette::Inactive, QPalette::HighlightedText, brush1);
}
view1.setPalette(palette1);
view1.setHtml(html);
view1.page()->setViewportSize(view1.page()->contentsSize());
view1.show();
QTest::qWaitForWindowExposed(&view1);
if (!active) {
controlView.show();
QTest::qWaitForWindowExposed(&controlView);
activeView = &controlView;
controlView.activateWindow();
} else {
view1.activateWindow();
activeView = &view1;
}
QTRY_COMPARE(QApplication::activeWindow(), activeView);
view1.page()->triggerAction(QWebEnginePage::SelectAll);
QImage img1(view1.page()->viewportSize(), QImage::Format_ARGB32);
QPainter painter1(&img1);
view1.page()->render(&painter1);
painter1.end();
view1.close();
controlView.close();
QWebEngineView view2;
QPalette palette2;
QBrush brush2(Qt::blue);
brush2.setStyle(Qt::SolidPattern);
if (active && background) {
// Rendered image must have blue background on an active QWebEngineView.
palette2.setBrush(QPalette::Active, QPalette::Highlight, brush2);
} else if (active && !background) {
// Rendered image must have blue foreground on an active QWebEngineView.
palette2.setBrush(QPalette::Active, QPalette::HighlightedText, brush2);
} else if (!active && background) {
// Rendered image must have blue background on an inactive QWebEngineView.
palette2.setBrush(QPalette::Inactive, QPalette::Highlight, brush2);
} else if (!active && !background) {
// Rendered image must have blue foreground on an inactive QWebEngineView.
palette2.setBrush(QPalette::Inactive, QPalette::HighlightedText, brush2);
}
view2.setPalette(palette2);
view2.setHtml(html);
view2.page()->setViewportSize(view2.page()->contentsSize());
view2.show();
QTest::qWaitForWindowExposed(&view2);
if (!active) {
controlView.show();
QTest::qWaitForWindowExposed(&controlView);
activeView = &controlView;
controlView.activateWindow();
} else {
view2.activateWindow();
activeView = &view2;
}
QTRY_COMPARE(QApplication::activeWindow(), activeView);
view2.page()->triggerAction(QWebEnginePage::SelectAll);
QImage img2(view2.page()->viewportSize(), QImage::Format_ARGB32);
QPainter painter2(&img2);
view2.page()->render(&painter2);
painter2.end();
view2.close();
controlView.close();
QVERIFY(img1 != img2);
#endif
}
#endif
void tst_QWebEngineView::renderingAfterMaxAndBack()
{
#if !defined(QWEBENGINEPAGE_RENDER)
QSKIP("QWEBENGINEPAGE_RENDER");
#else
QUrl url = QUrl("data:text/html,<html><head></head>"
"<body width=1024 height=768 bgcolor=red>"
"</body>"
"</html>");
QWebEngineView view;
view.page()->load(url);
QSignalSpy spyFinished(&view, &QWebEngineView::loadFinished);
QVERIFY(spyFinished.wait());
view.show();
view.page()->settings()->setMaximumPagesInCache(3);
QTest::qWaitForWindowExposed(&view);
QPixmap reference(view.page()->viewportSize());
reference.fill(Qt::red);
QPixmap image(view.page()->viewportSize());
QPainter painter(&image);
view.page()->render(&painter);
QCOMPARE(image, reference);
QUrl url2 = QUrl("data:text/html,<html><head></head>"
"<body width=1024 height=768 bgcolor=blue>"
"</body>"
"</html>");
view.page()->load(url2);
QVERIFY(spyFinished.wait());
view.showMaximized();
QTest::qWaitForWindowExposed(&view);
QPixmap reference2(view.page()->viewportSize());
reference2.fill(Qt::blue);
QPixmap image2(view.page()->viewportSize());
QPainter painter2(&image2);
view.page()->render(&painter2);
QCOMPARE(image2, reference2);
view.back();
QPixmap reference3(view.page()->viewportSize());
reference3.fill(Qt::red);
QPixmap image3(view.page()->viewportSize());
QPainter painter3(&image3);
view.page()->render(&painter3);
QCOMPARE(image3, reference3);
#endif
}
class KeyboardAndMouseEventRecordingWidget : public QWidget {
public:
explicit KeyboardAndMouseEventRecordingWidget(QWidget *parent = 0) :
QWidget(parent), m_eventCounter(0) {}
bool event(QEvent *event) override
{
QString eventString;
switch (event->type()) {
case QEvent::TabletPress:
case QEvent::TabletRelease:
case QEvent::TabletMove:
case QEvent::MouseButtonPress:
case QEvent::MouseButtonRelease:
case QEvent::MouseButtonDblClick:
case QEvent::MouseMove:
case QEvent::TouchBegin:
case QEvent::TouchUpdate:
case QEvent::TouchEnd:
case QEvent::TouchCancel:
case QEvent::ContextMenu:
case QEvent::KeyPress:
case QEvent::KeyRelease:
#ifndef QT_NO_WHEELEVENT
case QEvent::Wheel:
#endif
++m_eventCounter;
event->setAccepted(true);
QDebug(&eventString) << event;
m_eventHistory.append(eventString);
return true;
default:
break;
}
return QWidget::event(event);
}
void clearEventCount()
{
m_eventCounter = 0;
}
int eventCount()
{
return m_eventCounter;
}
void printEventHistory()
{
qDebug() << "Received events are:";
for (int i = 0; i < m_eventHistory.size(); ++i) {
qDebug() << m_eventHistory[i];
}
}
private:
int m_eventCounter;
QVector<QString> m_eventHistory;
};
void tst_QWebEngineView::doNotSendMouseKeyboardEventsWhenDisabled()
{
QFETCH(bool, viewEnabled);
QFETCH(int, resultEventCount);
KeyboardAndMouseEventRecordingWidget parentWidget;
parentWidget.resize(640, 480);
QWebEngineView webView(&parentWidget);
webView.setEnabled(viewEnabled);
parentWidget.setLayout(new QStackedLayout);
parentWidget.layout()->addWidget(&webView);
webView.resize(640, 480);
parentWidget.show();
QVERIFY(QTest::qWaitForWindowExposed(&webView));
QSignalSpy loadSpy(&webView, SIGNAL(loadFinished(bool)));
webView.setHtml("<html><head><title>Title</title></head><body>Hello"
"<input id=\"input\" type=\"text\"></body></html>");
QTRY_COMPARE(loadSpy.count(), 1);
// When the webView is enabled, the events are swallowed by it, and the parent widget
// does not receive any events, otherwise all events are processed by the parent widget.
parentWidget.clearEventCount();
QTest::mousePress(parentWidget.windowHandle(), Qt::LeftButton);
QTest::mouseMove(parentWidget.windowHandle(), QPoint(100, 100));
QTest::mouseRelease(parentWidget.windowHandle(), Qt::LeftButton,
Qt::KeyboardModifiers(), QPoint(100, 100));
// Wait a bit for the mouse events to be processed, so they don't interfere with the js focus
// below.
QTest::qWait(100);
evaluateJavaScriptSync(webView.page(), "document.getElementById(\"input\").focus()");
QTest::keyPress(parentWidget.windowHandle(), Qt::Key_H);
// Wait a bit for the key press to be handled. We have to do it, because the compare
// below could immediately finish successfully, without alloing for the events to be handled.
QTest::qWait(100);
QTRY_COMPARE_WITH_TIMEOUT_FAIL_BLOCK(parentWidget.eventCount(), resultEventCount,
1000, parentWidget.printEventHistory(););
}
void tst_QWebEngineView::doNotSendMouseKeyboardEventsWhenDisabled_data()
{
QTest::addColumn<bool>("viewEnabled");
QTest::addColumn<int>("resultEventCount");
QTest::newRow("enabled view receives events") << true << 0;
QTest::newRow("disabled view does not receive events") << false << 4;
}
void tst_QWebEngineView::stopSettingFocusWhenDisabled()
{
QFETCH(bool, viewEnabled);
QFETCH(bool, focusResult);
QWebEngineView webView;
webView.settings()->setAttribute(QWebEngineSettings::FocusOnNavigationEnabled, true);
webView.resize(640, 480);
webView.show();
webView.setEnabled(viewEnabled);
QVERIFY(QTest::qWaitForWindowExposed(&webView));
QSignalSpy loadSpy(&webView, SIGNAL(loadFinished(bool)));
webView.setHtml("<html><head><title>Title</title></head><body>Hello"
"<input id=\"input\" type=\"text\"></body></html>");
QTRY_COMPARE(loadSpy.count(), 1);
QTRY_COMPARE_WITH_TIMEOUT(webView.hasFocus(), focusResult, 1000);
evaluateJavaScriptSync(webView.page(), "document.getElementById(\"input\").focus()");
QTRY_COMPARE_WITH_TIMEOUT(webView.hasFocus(), focusResult, 1000);
}
void tst_QWebEngineView::stopSettingFocusWhenDisabled_data()
{
QTest::addColumn<bool>("viewEnabled");
QTest::addColumn<bool>("focusResult");
QTest::newRow("enabled view gets focus") << true << true;
QTest::newRow("disabled view does not get focus") << false << false;
}
void tst_QWebEngineView::focusOnNavigation_data()
{
QTest::addColumn<bool>("focusOnNavigation");
QTest::addColumn<bool>("viewReceivedFocus");
QTest::newRow("focusOnNavigation true") << true << true;
QTest::newRow("focusOnNavigation false") << false << false;
}
void tst_QWebEngineView::focusOnNavigation()
{
QFETCH(bool, focusOnNavigation);
QFETCH(bool, viewReceivedFocus);
#define triggerJavascriptFocus()\
evaluateJavaScriptSync(webView->page(), "document.getElementById(\"input\").focus()");
#define loadAndTriggerFocusAndCompare()\
QTRY_COMPARE(loadSpy.count(), 1);\
triggerJavascriptFocus();\
QTRY_COMPARE(webView->hasFocus(), viewReceivedFocus);
// Create a container widget, that will hold a line edit that has initial focus, and a web
// engine view.
QScopedPointer<QWidget> containerWidget(new QWidget);
QLineEdit *label = new QLineEdit;
label->setText(QString::fromLatin1("Text"));
label->setFocus();
// Create the web view, and set its focusOnNavigation property.
QWebEngineView *webView = new QWebEngineView;
QWebEngineSettings *settings = webView->page()->settings();
settings->setAttribute(QWebEngineSettings::FocusOnNavigationEnabled, focusOnNavigation);
webView->resize(300, 300);
QHBoxLayout *layout = new QHBoxLayout;
layout->addWidget(label);
layout->addWidget(webView);
containerWidget->setLayout(layout);
containerWidget->show();
QVERIFY(QTest::qWaitForWindowExposed(containerWidget.data()));
// Load the content, invoke javascript focus on the view, and check which widget has focus.
QSignalSpy loadSpy(webView, SIGNAL(loadFinished(bool)));
webView->setHtml("<html><head><title>Title</title></head><body>Hello"
"<input id=\"input\" type=\"text\"></body></html>");
loadAndTriggerFocusAndCompare();
// Load a different page, and check focus.
loadSpy.clear();
webView->setHtml("<html><head><title>Title</title></head><body>Hello 2"
"<input id=\"input\" type=\"text\"></body></html>");
loadAndTriggerFocusAndCompare();
// Navigate to previous page in history, check focus.
loadSpy.clear();
webView->triggerPageAction(QWebEnginePage::Back);
loadAndTriggerFocusAndCompare();
// Navigate to next page in history, check focus.
loadSpy.clear();
webView->triggerPageAction(QWebEnginePage::Forward);
loadAndTriggerFocusAndCompare();
// Reload page, check focus.
loadSpy.clear();
webView->triggerPageAction(QWebEnginePage::Reload);
loadAndTriggerFocusAndCompare();
// Reload page bypassing cache, check focus.
loadSpy.clear();
webView->triggerPageAction(QWebEnginePage::ReloadAndBypassCache);
loadAndTriggerFocusAndCompare();
// Manually forcing focus on web view should work.
webView->setFocus();
QTRY_COMPARE(webView->hasFocus(), true);
// Clean up.
#undef loadAndTriggerFocusAndCompare
#undef triggerJavascriptFocus
}
void tst_QWebEngineView::focusInternalRenderWidgetHostViewQuickItem()
{
// Create a container widget, that will hold a line edit that has initial focus, and a web
// engine view.
QScopedPointer<QWidget> containerWidget(new QWidget);
QLineEdit *label = new QLineEdit;
label->setText(QString::fromLatin1("Text"));
label->setFocus();
// Create the web view, and set its focusOnNavigation property to false, so it doesn't
// get initial focus.
QWebEngineView *webView = new QWebEngineView;
QWebEngineSettings *settings = webView->page()->settings();
settings->setAttribute(QWebEngineSettings::FocusOnNavigationEnabled, false);
webView->resize(300, 300);
QHBoxLayout *layout = new QHBoxLayout;
layout->addWidget(label);
layout->addWidget(webView);
containerWidget->setLayout(layout);
containerWidget->show();
QVERIFY(QTest::qWaitForWindowExposed(containerWidget.data()));
// Load the content, and check that focus is not set.
QSignalSpy loadSpy(webView, SIGNAL(loadFinished(bool)));
webView->setHtml("<html><head><title>Title</title></head><body>Hello"
"<input id=\"input\" type=\"text\"></body></html>");
QTRY_COMPARE(loadSpy.count(), 1);
QTRY_COMPARE(webView->hasFocus(), false);
// Manually trigger focus.
webView->setFocus();
// Check that focus is set in QWebEngineView and all internal classes.
QTRY_COMPARE(webView->hasFocus(), true);
QQuickWidget *renderWidgetHostViewQtDelegateWidget =
qobject_cast<QQuickWidget *>(webView->focusProxy());
QVERIFY(renderWidgetHostViewQtDelegateWidget);
QTRY_COMPARE(renderWidgetHostViewQtDelegateWidget->hasFocus(), true);
QQuickItem *renderWidgetHostViewQuickItem =
renderWidgetHostViewQtDelegateWidget->rootObject();
QVERIFY(renderWidgetHostViewQuickItem);
QTRY_COMPARE(renderWidgetHostViewQuickItem->hasFocus(), true);
}
void tst_QWebEngineView::doNotBreakLayout()
{
QScopedPointer<QWidget> containerWidget(new QWidget);
QHBoxLayout *layout = new QHBoxLayout;
layout->addWidget(new QWidget);
layout->addWidget(new QWidget);
layout->addWidget(new QWidget);
layout->addWidget(new QWebEngineView);
containerWidget->setLayout(layout);
containerWidget->setGeometry(50, 50, 800, 600);
containerWidget->show();
QVERIFY(QTest::qWaitForWindowExposed(containerWidget.data()));
QSize previousSize = static_cast<QWidgetItem *>(layout->itemAt(0))->widget()->size();
for (int i = 1; i < layout->count(); i++) {
QSize actualSize = static_cast<QWidgetItem *>(layout->itemAt(i))->widget()->size();
// There could be smaller differences on some platforms
QVERIFY(qAbs(previousSize.width() - actualSize.width()) <= 2);
QVERIFY(qAbs(previousSize.height() - actualSize.height()) <= 2);
previousSize = actualSize;
}
}
void tst_QWebEngineView::changeLocale()
{
QStringList errorLines;
QUrl url("http://non.existent/");
QLocale::setDefault(QLocale("de"));
QWebEngineView viewDE;
QSignalSpy loadFinishedSpyDE(&viewDE, SIGNAL(loadFinished(bool)));
viewDE.load(url);
QTRY_COMPARE_WITH_TIMEOUT(loadFinishedSpyDE.count(), 1, 20000);
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"));
QLocale::setDefault(QLocale("en"));
QWebEngineView viewEN;
QSignalSpy loadFinishedSpyEN(&viewEN, SIGNAL(loadFinished(bool)));
viewEN.load(url);
QTRY_COMPARE_WITH_TIMEOUT(loadFinishedSpyEN.count(), 1, 20000);
QTRY_VERIFY(!toPlainTextSync(viewEN.page()).isEmpty());
errorLines = toPlainTextSync(viewEN.page()).split(QRegularExpression("[\r\n]"), Qt::SkipEmptyParts);
QCOMPARE(errorLines.first().toUtf8(), QByteArrayLiteral("This site can\xE2\x80\x99t be reached"));
// Reset error page
viewDE.load(QUrl("about:blank"));
QVERIFY(loadFinishedSpyDE.wait());
loadFinishedSpyDE.clear();
// Check whether an existing QWebEngineView keeps the language settings after changing the default locale
viewDE.load(url);
QTRY_COMPARE_WITH_TIMEOUT(loadFinishedSpyDE.count(), 1, 20000);
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"));
}
void tst_QWebEngineView::inputMethodsTextFormat_data()
{
QTest::addColumn<QString>("string");
QTest::addColumn<int>("start");
QTest::addColumn<int>("length");
QTest::addColumn<int>("underlineStyle");
QTest::addColumn<QColor>("underlineColor");
QTest::addColumn<QColor>("backgroundColor");
QTest::newRow("") << QString("") << 0 << 0 << static_cast<int>(QTextCharFormat::SingleUnderline) << QColor("red") << QColor();
QTest::newRow("Q") << QString("Q") << 0 << 1 << static_cast<int>(QTextCharFormat::SingleUnderline) << QColor("red") << QColor();
QTest::newRow("Qt") << QString("Qt") << 0 << 1 << static_cast<int>(QTextCharFormat::SingleUnderline) << QColor("red") << QColor();
QTest::newRow("Qt") << QString("Qt") << 0 << 2 << static_cast<int>(QTextCharFormat::SingleUnderline) << QColor("red") << QColor();
QTest::newRow("Qt") << QString("Qt") << 1 << 1 << static_cast<int>(QTextCharFormat::SingleUnderline) << QColor("red") << QColor();
QTest::newRow("Qt ") << QString("Qt ") << 0 << 1 << static_cast<int>(QTextCharFormat::SingleUnderline) << QColor("red") << QColor();
QTest::newRow("Qt ") << QString("Qt ") << 1 << 1 << static_cast<int>(QTextCharFormat::SingleUnderline) << QColor("red") << QColor();
QTest::newRow("Qt ") << QString("Qt ") << 2 << 1 << static_cast<int>(QTextCharFormat::SingleUnderline) << QColor("red") << QColor();
QTest::newRow("Qt ") << QString("Qt ") << 2 << -1 << static_cast<int>(QTextCharFormat::SingleUnderline) << QColor("red") << QColor();
QTest::newRow("Qt ") << QString("Qt ") << -2 << 3 << static_cast<int>(QTextCharFormat::SingleUnderline) << QColor("red") << QColor();
QTest::newRow("Qt ") << QString("Qt ") << -1 << -1 << static_cast<int>(QTextCharFormat::SingleUnderline) << QColor("red") << QColor();
QTest::newRow("Qt ") << QString("Qt ") << 0 << 3 << static_cast<int>(QTextCharFormat::SingleUnderline) << QColor("red") << QColor();
QTest::newRow("The Qt") << QString("The Qt") << 0 << 1 << static_cast<int>(QTextCharFormat::SingleUnderline) << QColor("red") << QColor();
QTest::newRow("The Qt Company") << QString("The Qt Company") << 0 << 1 << static_cast<int>(QTextCharFormat::SingleUnderline) << QColor("red") << QColor();
QTest::newRow("The Qt Company") << QString("The Qt Company") << 0 << 3 << static_cast<int>(QTextCharFormat::SingleUnderline) << QColor("green") << QColor();
QTest::newRow("The Qt Company") << QString("The Qt Company") << 4 << 2 << static_cast<int>(QTextCharFormat::SingleUnderline) << QColor("green") << QColor("red");
QTest::newRow("The Qt Company") << QString("The Qt Company") << 7 << 7 << static_cast<int>(QTextCharFormat::NoUnderline) << QColor("green") << QColor("red");
QTest::newRow("The Qt Company") << QString("The Qt Company") << 7 << 7 << static_cast<int>(QTextCharFormat::NoUnderline) << QColor() << QColor("red");
}
void tst_QWebEngineView::inputMethodsTextFormat()
{
QWebEngineView view;
view.settings()->setAttribute(QWebEngineSettings::FocusOnNavigationEnabled, true);
QSignalSpy loadFinishedSpy(&view, SIGNAL(loadFinished(bool)));
view.setHtml("<html><body>"
" <input type='text' id='input1' style='font-family: serif' value='' maxlength='20'/>"
"</body></html>");
QTRY_COMPARE(loadFinishedSpy.count(), 1);
evaluateJavaScriptSync(view.page(), "document.getElementById('input1').focus()");
view.show();
QFETCH(QString, string);
QFETCH(int, start);
QFETCH(int, length);
QFETCH(int, underlineStyle);
QFETCH(QColor, underlineColor);
QFETCH(QColor, backgroundColor);
QList<QInputMethodEvent::Attribute> attrs;
QTextCharFormat format;
format.setUnderlineStyle(static_cast<QTextCharFormat::UnderlineStyle>(underlineStyle));
format.setUnderlineColor(underlineColor);
// Setting background color is disabled for Qt WebEngine because some IME manager
// sets background color to black and there is no API for setting the foreground color.
// This may result black text on black background. However, we still test it to ensure
// changing background color doesn't cause any crash.
if (backgroundColor.isValid())
format.setBackground(QBrush(backgroundColor));
attrs.append(QInputMethodEvent::Attribute(QInputMethodEvent::TextFormat, start, length, format));
QInputMethodEvent im(string, attrs);
QVERIFY(QApplication::sendEvent(view.focusProxy(), &im));
QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.getElementById('input1').value").toString(), string);
}
void tst_QWebEngineView::keyboardEvents()
{
QWebEngineView view;
view.show();
QSignalSpy loadFinishedSpy(&view, SIGNAL(loadFinished(bool)));
view.load(QUrl("qrc:///resources/keyboardEvents.html"));
QVERIFY(loadFinishedSpy.wait());
QStringList elements;
elements << "first_div" << "second_div";
elements << "text_input" << "radio1" << "checkbox1" << "checkbox2";
elements << "number_input" << "range_input" << "search_input";
elements << "submit_button" << "combobox" << "first_hyperlink" << "second_hyperlink";
// Iterate over the elements of the test page with the Tab key. This tests whether any
// element blocks the in-page navigation by Tab.
for (const QString &elementId : elements) {
QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString(), elementId);
QTest::keyPress(view.focusProxy(), Qt::Key_Tab);
}
// Move back to the radio buttons with the Shift+Tab key combination
for (int i = 0; i < 10; ++i)
QTest::keyPress(view.focusProxy(), Qt::Key_Tab, Qt::ShiftModifier);
QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString(), QStringLiteral("radio2"));
// Test the Space key by checking a radio button
QVERIFY(!evaluateJavaScriptSync(view.page(), "document.getElementById('radio2').checked").toBool());
QTest::keyClick(view.focusProxy(), Qt::Key_Space);
QTRY_VERIFY(evaluateJavaScriptSync(view.page(), "document.getElementById('radio2').checked").toBool());
// Test the Left key by switching the radio button
QVERIFY(!evaluateJavaScriptSync(view.page(), "document.getElementById('radio1').checked").toBool());
QTest::keyPress(view.focusProxy(), Qt::Key_Left);
QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString(), QStringLiteral("radio1"));
QVERIFY(!evaluateJavaScriptSync(view.page(), "document.getElementById('radio2').checked").toBool());
QVERIFY(evaluateJavaScriptSync(view.page(), "document.getElementById('radio1').checked").toBool());
// Test the Space key by unchecking a checkbox
evaluateJavaScriptSync(view.page(), "document.getElementById('checkbox1').focus()");
QVERIFY(evaluateJavaScriptSync(view.page(), "document.getElementById('checkbox1').checked").toBool());
QTest::keyClick(view.focusProxy(), Qt::Key_Space);
QTRY_VERIFY(!evaluateJavaScriptSync(view.page(), "document.getElementById('checkbox1').checked").toBool());
// Test the Up and Down keys by changing the value of a spinbox
evaluateJavaScriptSync(view.page(), "document.getElementById('number_input').focus()");
QCOMPARE(evaluateJavaScriptSync(view.page(), "document.getElementById('number_input').value").toInt(), 5);
QTest::keyPress(view.focusProxy(), Qt::Key_Up);
QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.getElementById('number_input').value").toInt(), 6);
QTest::keyPress(view.focusProxy(), Qt::Key_Down);
QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.getElementById('number_input').value").toInt(), 5);
// Test the Left, Right, Home, PageUp, End and PageDown keys by changing the value of a slider
evaluateJavaScriptSync(view.page(), "document.getElementById('range_input').focus()");
QCOMPARE(evaluateJavaScriptSync(view.page(), "document.getElementById('range_input').value").toString(), QStringLiteral("5"));
QTest::keyPress(view.focusProxy(), Qt::Key_Left);
QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.getElementById('range_input').value").toString(), QStringLiteral("4"));
QTest::keyPress(view.focusProxy(), Qt::Key_Right);
QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.getElementById('range_input').value").toString(), QStringLiteral("5"));
QTest::keyPress(view.focusProxy(), Qt::Key_Home);
QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.getElementById('range_input').value").toString(), QStringLiteral("0"));
QTest::keyPress(view.focusProxy(), Qt::Key_PageUp);
QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.getElementById('range_input').value").toString(), QStringLiteral("1"));
QTest::keyPress(view.focusProxy(), Qt::Key_End);
QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.getElementById('range_input').value").toString(), QStringLiteral("10"));
QTest::keyPress(view.focusProxy(), Qt::Key_PageDown);
QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.getElementById('range_input').value").toString(), QStringLiteral("9"));
// Test the Escape key by removing the content of a search field
evaluateJavaScriptSync(view.page(), "document.getElementById('search_input').focus()");
QCOMPARE(evaluateJavaScriptSync(view.page(), "document.getElementById('search_input').value").toString(), QStringLiteral("test"));
QTest::keyPress(view.focusProxy(), Qt::Key_Escape);
QTRY_VERIFY(evaluateJavaScriptSync(view.page(), "document.getElementById('search_input').value").toString().isEmpty());
// Test the alpha keys by changing the values in a combobox
evaluateJavaScriptSync(view.page(), "document.getElementById('combobox').focus()");
QCOMPARE(evaluateJavaScriptSync(view.page(), "document.getElementById('combobox').value").toString(), QStringLiteral("a"));
QTest::keyPress(view.focusProxy(), Qt::Key_B);
QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.getElementById('combobox').value").toString(), QStringLiteral("b"));
// Must wait with the second key press to simulate selection of another element
QTest::keyPress(view.focusProxy(), Qt::Key_C, Qt::NoModifier, 1100 /* blink::typeAheadTimeout + 0.1s */);
QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.getElementById('combobox').value").toString(), QStringLiteral("c"));
// Test the Enter key by loading a page with a hyperlink
evaluateJavaScriptSync(view.page(), "document.getElementById('first_hyperlink').focus()");
QTest::keyPress(view.focusProxy(), Qt::Key_Enter);
QVERIFY(loadFinishedSpy.wait());
}
class WebViewWithUrlBar : public QWidget {
public:
QLineEdit *lineEdit = new QLineEdit;
QCompleter *urlCompleter = new QCompleter({ QStringLiteral("test") }, lineEdit);
QWebEngineView *webView = new QWebEngineView;
QVBoxLayout *layout = new QVBoxLayout;
WebViewWithUrlBar()
{
resize(500, 500);
setLayout(layout);
layout->addWidget(lineEdit);
layout->addWidget(webView);
lineEdit->setCompleter(urlCompleter);
lineEdit->setFocus();
}
};
void tst_QWebEngineView::keyboardFocusAfterPopup()
{
const QString html = QStringLiteral(
"<html>"
" <body onload=\"document.getElementById('input1').focus()\">"
" <input id=input1 type=text/>"
" </body>"
"</html>");
WebViewWithUrlBar window;
QSignalSpy loadFinishedSpy(window.webView, &QWebEngineView::loadFinished);
connect(window.lineEdit, &QLineEdit::editingFinished, [&] { window.webView->setHtml(html); });
window.webView->settings()->setAttribute(QWebEngineSettings::FocusOnNavigationEnabled, true);
window.show();
// Focus will initially go to the QLineEdit.
QTRY_COMPARE(QApplication::focusWidget(), window.lineEdit);
// Trigger QCompleter's popup and select the first suggestion.
QTest::keyClick(QApplication::focusWindow(), Qt::Key_T);
QTRY_VERIFY(QApplication::activePopupWidget());
QTest::keyClick(QApplication::focusWindow(), Qt::Key_Down);
QTest::keyClick(QApplication::focusWindow(), Qt::Key_Enter);
// Due to FocusOnNavigationEnabled, focus should now move to the webView.
QTRY_COMPARE(QApplication::focusWidget(), window.webView->focusProxy());
// Keyboard events sent to the window should go to the <input> element.
QVERIFY(loadFinishedSpy.count() || loadFinishedSpy.wait());
QTest::keyClick(QApplication::focusWindow(), Qt::Key_X);
QTRY_COMPARE(evaluateJavaScriptSync(window.webView->page(), "document.getElementById('input1').value").toString(),
QStringLiteral("x"));
}
void tst_QWebEngineView::mouseClick()
{
QWebEngineView view;
view.show();
view.resize(200, 200);
QVERIFY(QTest::qWaitForWindowExposed(&view));
QSignalSpy loadFinishedSpy(&view, SIGNAL(loadFinished(bool)));
QSignalSpy selectionChangedSpy(&view, SIGNAL(selectionChanged()));
QPoint textInputCenter;
// Single Click
view.settings()->setAttribute(QWebEngineSettings::FocusOnNavigationEnabled, false);
selectionChangedSpy.clear();
view.setHtml("<html><body>"
"<form><input id='input' width='150' type='text' value='The Qt Company' /></form>"
"</body></html>");
QVERIFY(loadFinishedSpy.wait());
QVERIFY(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString().isEmpty());
textInputCenter = elementCenter(view.page(), "input");
QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, textInputCenter);
QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString(), QStringLiteral("input"));
QCOMPARE(selectionChangedSpy.count(), 0);
QVERIFY(view.focusProxy()->inputMethodQuery(Qt::ImCurrentSelection).toString().isEmpty());
// Double click
view.settings()->setAttribute(QWebEngineSettings::FocusOnNavigationEnabled, true);
selectionChangedSpy.clear();
view.setHtml("<html><body onload='document.getElementById(\"input\").focus()'>"
"<form><input id='input' width='150' type='text' value='The Qt Company' /></form>"
"</body></html>");
QVERIFY(loadFinishedSpy.wait());
textInputCenter = elementCenter(view.page(), "input");
QTest::mouseMultiClick(view.focusProxy(), textInputCenter, 2);
QVERIFY(selectionChangedSpy.wait());
QCOMPARE(selectionChangedSpy.count(), 1);
QCOMPARE(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString(), QStringLiteral("input"));
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImCurrentSelection).toString(), QStringLiteral("Company"));
QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, textInputCenter);
QVERIFY(selectionChangedSpy.wait());
QCOMPARE(selectionChangedSpy.count(), 2);
QVERIFY(view.focusProxy()->inputMethodQuery(Qt::ImCurrentSelection).toString().isEmpty());
// Triple click
view.settings()->setAttribute(QWebEngineSettings::FocusOnNavigationEnabled, true);
selectionChangedSpy.clear();
view.setHtml("<html><body onload='document.getElementById(\"input\").focus()'>"
"<form><input id='input' width='150' type='text' value='The Qt Company' /></form>"
"</body></html>");
QVERIFY(loadFinishedSpy.wait());
textInputCenter = elementCenter(view.page(), "input");
QTest::mouseMultiClick(view.focusProxy(), textInputCenter, 3);
QVERIFY(selectionChangedSpy.wait());
QTRY_COMPARE(selectionChangedSpy.count(), 2);
QCOMPARE(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString(), QStringLiteral("input"));
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImCurrentSelection).toString(), QStringLiteral("The Qt Company"));
QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, textInputCenter);
QVERIFY(selectionChangedSpy.wait());
QCOMPARE(selectionChangedSpy.count(), 3);
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, 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(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(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(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()
{
QMap<QString, QString> postData;
// use reserved characters to make the test harder to pass
postData[QStringLiteral("Spä=m")] = QStringLiteral("ëgg:s");
postData[QStringLiteral("foo\r\n")] = QStringLiteral("ba&r");
QEventLoop eventloop;
// Set up dummy "HTTP" server
QTcpServer server;
connect(&server, &QTcpServer::newConnection, this, [this, &server, &eventloop, &postData](){
QTcpSocket* socket = server.nextPendingConnection();
connect(socket, &QAbstractSocket::disconnected, this, [&eventloop](){
eventloop.quit();
});
connect(socket, &QIODevice::readyRead, this, [socket, &server, &postData](){
QByteArray rawData = socket->readAll();
QStringList lines = QString::fromLocal8Bit(rawData).split("\r\n");
// examine request
QStringList request = lines[0].split(" ", Qt::SkipEmptyParts);
bool requestOk = request.length() > 2
&& request[2].toUpper().startsWith("HTTP/")
&& request[0].toUpper() == "POST"
&& request[1] == "/";
if (!requestOk) // POST and HTTP/... can be switched(?)
requestOk = request.length() > 2
&& request[0].toUpper().startsWith("HTTP/")
&& request[2].toUpper() == "POST"
&& request[1] == "/";
// examine headers
int line = 1;
bool headersOk = true;
for (; headersOk && line < lines.length(); line++) {
QStringList headerParts = lines[line].split(":");
if (headerParts.length() < 2)
break;
QString headerKey = headerParts[0].trimmed().toLower();
QString headerValue = headerParts[1].trimmed().toLower();
if (headerKey == "host")
headersOk = headersOk && (headerValue == "127.0.0.1")
&& (headerParts.length() == 3)
&& (headerParts[2].trimmed()
== QString::number(server.serverPort()));
if (headerKey == "content-type")
headersOk = headersOk && (headerValue == "application/x-www-form-urlencoded");
}
// examine body
bool bodyOk = true;
if (lines.length() == line+2) {
QStringList postedFields = lines[line+1].split("&");
QMap<QString, QString> postedData;
for (int i = 0; bodyOk && i < postedFields.length(); i++) {
QStringList postedField = postedFields[i].split("=");
if (postedField.length() == 2)
postedData[QUrl::fromPercentEncoding(postedField[0].toLocal8Bit())]
= QUrl::fromPercentEncoding(postedField[1].toLocal8Bit());
else
bodyOk = false;
}
bodyOk = bodyOk && (postedData == postData);
} else { // no body at all or more than 1 line
bodyOk = false;
}
// send response
socket->write("HTTP/1.1 200 OK\r\n");
socket->write("Content-Type: text/html\r\n");
socket->write("Content-Length: 39\r\n\r\n");
if (requestOk && headersOk && bodyOk)
// 6 6 11 7 7 2 = 39 (Content-Length)
socket->write("<html><body>Test Passed</body></html>\r\n");
else
socket->write("<html><body>Test Failed</body></html>\r\n");
socket->flush();
if (!requestOk || !headersOk || !bodyOk) {
qDebug() << "Dummy HTTP Server: received request was not as expected";
qDebug() << rawData;
QVERIFY(requestOk); // one of them will yield useful output and make the test fail
QVERIFY(headersOk);
QVERIFY(bodyOk);
}
socket->close();
});
});
if (!server.listen())
QFAIL("Dummy HTTP Server: listen() failed");
// Manual, hard coded client (commented out, but not removed - for reference and just in case)
/*
QTcpSocket client;
connect(&client, &QIODevice::readyRead, this, [&client, &eventloop](){
qDebug() << "Dummy HTTP client: data received";
qDebug() << client.readAll();
eventloop.quit();
});
connect(&client, &QAbstractSocket::connected, this, [&client](){
client.write("HTTP/1.1 / GET\r\n\r\n");
});
client.connectToHost(QHostAddress::LocalHost, server.serverPort());
*/
// send the POST request
QWebEngineView view;
QString sPort = QString::number(server.serverPort());
view.load(QWebEngineHttpRequest::postRequest(QUrl("http://127.0.0.1:"+sPort), postData));
// timeout after 10 seconds
QTimer timeoutGuard(this);
connect(&timeoutGuard, &QTimer::timeout, this, [&eventloop](){
eventloop.quit();
QFAIL("Dummy HTTP Server: waiting for data timed out");
});
timeoutGuard.setSingleShot(true);
timeoutGuard.start(10000);
// start the test
eventloop.exec();
timeoutGuard.stop();
server.close();
}
void tst_QWebEngineView::inputFieldOverridesShortcuts()
{
bool actionTriggered = false;
QAction *action = new QAction;
connect(action, &QAction::triggered, [&actionTriggered] () { actionTriggered = true; });
QWebEngineView view;
view.addAction(action);
QSignalSpy loadFinishedSpy(&view, SIGNAL(loadFinished(bool)));
view.setHtml(QString("<html><body>"
"<button id=\"btn1\" type=\"button\">push it real good</button>"
"<input id=\"input1\" type=\"text\" value=\"x\">"
"<input id=\"pass1\" type=\"password\" value=\"x\">"
"</body></html>"));
QVERIFY(loadFinishedSpy.wait());
view.show();
QVERIFY(QTest::qWaitForWindowActive(&view));
auto inputFieldValue = [&view] () -> QString {
return evaluateJavaScriptSync(view.page(),
"document.getElementById('input1').value").toString();
};
auto passwordFieldValue = [&view] () -> QString {
return evaluateJavaScriptSync(view.page(),
"document.getElementById('pass1').value").toString();
};
// The input form is not focused. The action is triggered on pressing Shift+Delete.
action->setShortcut(Qt::SHIFT + Qt::Key_Delete);
QTest::keyClick(view.windowHandle(), Qt::Key_Delete, Qt::ShiftModifier);
QTRY_VERIFY(actionTriggered);
QCOMPARE(inputFieldValue(), QString("x"));
// The input form is not focused. The action is triggered on pressing X.
action->setShortcut(Qt::Key_X);
actionTriggered = false;
QTest::keyClick(view.windowHandle(), Qt::Key_X);
QTRY_VERIFY(actionTriggered);
QCOMPARE(inputFieldValue(), QString("x"));
// The input form is focused. The action is not triggered, and the form's text changed.
evaluateJavaScriptSync(view.page(), "document.getElementById('input1').focus();");
QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString(), QStringLiteral("input1"));
actionTriggered = false;
QTest::keyClick(view.windowHandle(), Qt::Key_Y);
QTRY_COMPARE(inputFieldValue(), QString("yx"));
QTest::keyClick(view.windowHandle(), Qt::Key_X);
QTRY_COMPARE(inputFieldValue(), QString("yxx"));
QVERIFY(!actionTriggered);
// The password input form is focused. The action is not triggered, and the form's text changed.
evaluateJavaScriptSync(view.page(), "document.getElementById('pass1').focus();");
QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString(), QStringLiteral("pass1"));
actionTriggered = false;
QTest::keyClick(view.windowHandle(), Qt::Key_Y);
QTRY_COMPARE(passwordFieldValue(), QString("yx"));
QTest::keyClick(view.windowHandle(), Qt::Key_X);
QTRY_COMPARE(passwordFieldValue(), QString("yxx"));
QVERIFY(!actionTriggered);
// The input form is focused. Make sure we don't override all short cuts.
// A Ctrl-1 action is no default Qt key binding and should be triggerable.
evaluateJavaScriptSync(view.page(), "document.getElementById('input1').focus();");
QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString(), QStringLiteral("input1"));
action->setShortcut(Qt::CTRL + Qt::Key_1);
QTest::keyClick(view.windowHandle(), Qt::Key_1, Qt::ControlModifier);
QTRY_VERIFY(actionTriggered);
QCOMPARE(inputFieldValue(), QString("yxx"));
// The input form is focused. The following shortcuts are not overridden
// thus handled by Qt WebEngine. Make sure the subsequent shortcuts with text
// character don't cause assert due to an unconsumed editor command.
QTest::keyClick(view.windowHandle(), Qt::Key_A, Qt::ControlModifier);
QTest::keyClick(view.windowHandle(), Qt::Key_C, Qt::ControlModifier);
QTest::keyClick(view.windowHandle(), Qt::Key_V, Qt::ControlModifier);
QTest::keyClick(view.windowHandle(), Qt::Key_V, Qt::ControlModifier);
QTRY_COMPARE(inputFieldValue(), QString("yxxyxx"));
// Remove focus from the input field. A QKeySequence::Copy action must be triggerable.
evaluateJavaScriptSync(view.page(), "document.getElementById('btn1').focus();");
QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString(), QStringLiteral("btn1"));
action->setShortcut(QKeySequence::Copy);
actionTriggered = false;
QTest::keyClick(view.windowHandle(), Qt::Key_C, Qt::ControlModifier);
QTRY_VERIFY(actionTriggered);
}
struct InputMethodInfo
{
InputMethodInfo(const int cursorPosition,
const int anchorPosition,
QString surroundingText,
QString selectedText)
: cursorPosition(cursorPosition)
, anchorPosition(anchorPosition)
, surroundingText(surroundingText)
, selectedText(selectedText)
{}
int cursorPosition;
int anchorPosition;
QString surroundingText;
QString selectedText;
};
class TestInputContext : public QPlatformInputContext
{
public:
TestInputContext()
: m_visible(false)
{
QInputMethodPrivate* inputMethodPrivate = QInputMethodPrivate::get(qApp->inputMethod());
inputMethodPrivate->testContext = this;
}
~TestInputContext()
{
QInputMethodPrivate* inputMethodPrivate = QInputMethodPrivate::get(qApp->inputMethod());
inputMethodPrivate->testContext = 0;
}
virtual void showInputPanel()
{
m_visible = true;
}
virtual void hideInputPanel()
{
m_visible = false;
}
virtual bool isInputPanelVisible() const
{
return m_visible;
}
virtual void update(Qt::InputMethodQueries queries)
{
if (!qApp->focusObject())
return;
if (!(queries & Qt::ImQueryInput))
return;
QInputMethodQueryEvent imQueryEvent(Qt::ImQueryInput);
QApplication::sendEvent(qApp->focusObject(), &imQueryEvent);
const int cursorPosition = imQueryEvent.value(Qt::ImCursorPosition).toInt();
const int anchorPosition = imQueryEvent.value(Qt::ImAnchorPosition).toInt();
QString surroundingText = imQueryEvent.value(Qt::ImSurroundingText).toString();
QString selectedText = imQueryEvent.value(Qt::ImCurrentSelection).toString();
infos.append(InputMethodInfo(cursorPosition, anchorPosition, surroundingText, selectedText));
}
bool m_visible;
QList<InputMethodInfo> infos;
};
void tst_QWebEngineView::softwareInputPanel()
{
TestInputContext testContext;
QWebEngineView view;
view.resize(640, 480);
view.show();
QSignalSpy loadFinishedSpy(&view, SIGNAL(loadFinished(bool)));
view.setHtml("<html><body>"
" <input type='text' id='input1' value='' size='50'/>"
"</body></html>");
QVERIFY(loadFinishedSpy.wait());
QPoint textInputCenter = elementCenter(view.page(), "input1");
QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, textInputCenter);
QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString(), QStringLiteral("input1"));
// This part of the test checks if the SIP (Software Input Panel) is triggered,
// which normally happens on mobile platforms, when a user input form receives
// a mouse click.
int inputPanel = view.style()->styleHint(QStyle::SH_RequestSoftwareInputPanel);
// For non-mobile platforms RequestSoftwareInputPanel event is not called
// because there is no SIP (Software Input Panel) triggered. In the case of a
// mobile platform, an input panel, e.g. virtual keyboard, is usually invoked
// and the RequestSoftwareInputPanel event is called. For these two situations
// this part of the test can verified as the checks below.
if (inputPanel)
QTRY_VERIFY(testContext.isInputPanelVisible());
else
QTRY_VERIFY(!testContext.isInputPanelVisible());
testContext.hideInputPanel();
QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, textInputCenter);
QTRY_VERIFY(testContext.isInputPanelVisible());
view.setHtml("<html><body><p id='para'>nothing to input here</p></body></html>");
QVERIFY(loadFinishedSpy.wait());
testContext.hideInputPanel();
QPoint paraCenter = elementCenter(view.page(), "para");
QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, paraCenter);
QVERIFY(!testContext.isInputPanelVisible());
// Check sending RequestSoftwareInputPanel event
view.page()->setHtml("<html><body>"
" <input type='text' id='input1' value='QtWebEngine inputMethod'/>"
" <div id='btnDiv' onclick='i=document.getElementById(&quot;input1&quot;); i.focus();'>abc</div>"
"</body></html>");
QVERIFY(loadFinishedSpy.wait());
QPoint btnDivCenter = elementCenter(view.page(), "btnDiv");
QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, btnDivCenter);
QVERIFY(!testContext.isInputPanelVisible());
}
void tst_QWebEngineView::inputContextQueryInput()
{
QWebEngineView view;
view.resize(640, 480);
view.show();
// testContext will be destroyed before the view, so no events are sent accidentally
// when the view is destroyed.
TestInputContext testContext;
QSignalSpy selectionChangedSpy(&view, SIGNAL(selectionChanged()));
QSignalSpy loadFinishedSpy(&view, SIGNAL(loadFinished(bool)));
view.setHtml("<html><body>"
" <input type='text' id='input1' value='' size='50'/>"
"</body></html>");
QTRY_COMPARE(loadFinishedSpy.count(), 1);
QCOMPARE(testContext.infos.count(), 0);
// Set focus on an input field.
QPoint textInputCenter = elementCenter(view.page(), "input1");
QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, textInputCenter);
QTRY_COMPARE(testContext.infos.count(), 2);
QCOMPARE(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString(), QStringLiteral("input1"));
foreach (const InputMethodInfo &info, testContext.infos) {
QCOMPARE(info.cursorPosition, 0);
QCOMPARE(info.anchorPosition, 0);
QCOMPARE(info.surroundingText, QStringLiteral(""));
QCOMPARE(info.selectedText, QStringLiteral(""));
}
testContext.infos.clear();
// Change content of an input field from JavaScript.
evaluateJavaScriptSync(view.page(), "document.getElementById('input1').value='QtWebEngine';");
QTRY_COMPARE(testContext.infos.count(), 1);
QCOMPARE(testContext.infos[0].cursorPosition, 11);
QCOMPARE(testContext.infos[0].anchorPosition, 11);
QCOMPARE(testContext.infos[0].surroundingText, QStringLiteral("QtWebEngine"));
QCOMPARE(testContext.infos[0].selectedText, QStringLiteral(""));
testContext.infos.clear();
// Change content of an input field by key press.
QTest::keyClick(view.focusProxy(), Qt::Key_Exclam);
QTRY_COMPARE(testContext.infos.count(), 1);
QCOMPARE(testContext.infos[0].cursorPosition, 12);
QCOMPARE(testContext.infos[0].anchorPosition, 12);
QCOMPARE(testContext.infos[0].surroundingText, QStringLiteral("QtWebEngine!"));
QCOMPARE(testContext.infos[0].selectedText, QStringLiteral(""));
testContext.infos.clear();
// Change cursor position.
QTest::keyClick(view.focusProxy(), Qt::Key_Left);
QTRY_COMPARE(testContext.infos.count(), 1);
QCOMPARE(testContext.infos[0].cursorPosition, 11);
QCOMPARE(testContext.infos[0].anchorPosition, 11);
QCOMPARE(testContext.infos[0].surroundingText, QStringLiteral("QtWebEngine!"));
QCOMPARE(testContext.infos[0].selectedText, QStringLiteral(""));
testContext.infos.clear();
// Selection by IME.
{
QList<QInputMethodEvent::Attribute> attributes;
QInputMethodEvent::Attribute newSelection(QInputMethodEvent::Selection, 2, 12, QVariant());
attributes.append(newSelection);
QInputMethodEvent event("", attributes);
QApplication::sendEvent(view.focusProxy(), &event);
}
QTRY_COMPARE(testContext.infos.count(), 2);
QTRY_COMPARE(selectionChangedSpy.count(), 1);
// As a first step, Chromium moves the cursor to the start of the selection.
// We don't filter this in QtWebEngine because we don't know yet if this is part of a selection.
QCOMPARE(testContext.infos[0].cursorPosition, 2);
QCOMPARE(testContext.infos[0].anchorPosition, 2);
QCOMPARE(testContext.infos[0].surroundingText, QStringLiteral("QtWebEngine!"));
QCOMPARE(testContext.infos[0].selectedText, QStringLiteral(""));
// The update of the selection.
QCOMPARE(testContext.infos[1].cursorPosition, 12);
QCOMPARE(testContext.infos[1].anchorPosition, 2);
QCOMPARE(testContext.infos[1].surroundingText, QStringLiteral("QtWebEngine!"));
QCOMPARE(testContext.infos[1].selectedText, QStringLiteral("WebEngine!"));
testContext.infos.clear();
selectionChangedSpy.clear();
// Clear selection by IME.
{
QList<QInputMethodEvent::Attribute> attributes;
QInputMethodEvent::Attribute newSelection(QInputMethodEvent::Selection, 0, 0, QVariant());
attributes.append(newSelection);
QInputMethodEvent event("", attributes);
QApplication::sendEvent(view.focusProxy(), &event);
}
QTRY_COMPARE(testContext.infos.count(), 1);
QTRY_COMPARE(selectionChangedSpy.count(), 1);
QCOMPARE(testContext.infos[0].cursorPosition, 0);
QCOMPARE(testContext.infos[0].anchorPosition, 0);
QCOMPARE(testContext.infos[0].surroundingText, QStringLiteral("QtWebEngine!"));
QCOMPARE(testContext.infos[0].selectedText, QStringLiteral(""));
testContext.infos.clear();
selectionChangedSpy.clear();
// Compose text.
{
QList<QInputMethodEvent::Attribute> attributes;
QInputMethodEvent event("123", attributes);
QApplication::sendEvent(view.focusProxy(), &event);
}
QTRY_COMPARE(testContext.infos.count(), 1);
QCOMPARE(testContext.infos[0].cursorPosition, 3);
QCOMPARE(testContext.infos[0].anchorPosition, 3);
QCOMPARE(testContext.infos[0].surroundingText, QStringLiteral("QtWebEngine!"));
QCOMPARE(testContext.infos[0].selectedText, QStringLiteral(""));
QCOMPARE(evaluateJavaScriptSync(view.page(), "document.getElementById('input1').value").toString(), QStringLiteral("123QtWebEngine!"));
testContext.infos.clear();
// Cancel composition.
{
QList<QInputMethodEvent::Attribute> attributes;
QInputMethodEvent event("", attributes);
QApplication::sendEvent(view.focusProxy(), &event);
}
QTRY_COMPARE(testContext.infos.count(), 2);
foreach (const InputMethodInfo &info, testContext.infos) {
QCOMPARE(info.cursorPosition, 0);
QCOMPARE(info.anchorPosition, 0);
QCOMPARE(info.surroundingText, QStringLiteral("QtWebEngine!"));
QCOMPARE(info.selectedText, QStringLiteral(""));
}
QCOMPARE(evaluateJavaScriptSync(view.page(), "document.getElementById('input1').value").toString(), QStringLiteral("QtWebEngine!"));
testContext.infos.clear();
// Commit text.
{
QList<QInputMethodEvent::Attribute> attributes;
QInputMethodEvent event("", attributes);
event.setCommitString(QStringLiteral("123"), 0, 0);
QApplication::sendEvent(view.focusProxy(), &event);
}
QTRY_COMPARE(testContext.infos.count(), 1);
QCOMPARE(testContext.infos[0].cursorPosition, 3);
QCOMPARE(testContext.infos[0].anchorPosition, 3);
QCOMPARE(testContext.infos[0].surroundingText, QStringLiteral("123QtWebEngine!"));
QCOMPARE(testContext.infos[0].selectedText, QStringLiteral(""));
QCOMPARE(evaluateJavaScriptSync(view.page(), "document.getElementById('input1').value").toString(), QStringLiteral("123QtWebEngine!"));
testContext.infos.clear();
// Focus out.
QTest::keyPress(view.focusProxy(), Qt::Key_Tab);
QTRY_COMPARE(testContext.infos.count(), 1);
QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString(), QStringLiteral(""));
testContext.infos.clear();
}
void tst_QWebEngineView::inputMethods()
{
QWebEngineView view;
view.settings()->setAttribute(QWebEngineSettings::FocusOnNavigationEnabled, true);
view.resize(640, 480);
view.show();
QSignalSpy selectionChangedSpy(&view, SIGNAL(selectionChanged()));
QSignalSpy loadFinishedSpy(&view, SIGNAL(loadFinished(bool)));
view.settings()->setFontFamily(QWebEngineSettings::SerifFont, view.settings()->fontFamily(QWebEngineSettings::FixedFont));
view.setHtml("<html><body>"
" <input type='text' id='input1' style='font-family: serif' value='' maxlength='20' size='50'/>"
"</body></html>");
QTRY_COMPARE(loadFinishedSpy.size(), 1);
QPoint textInputCenter = elementCenter(view.page(), "input1");
QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, textInputCenter);
QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString(), QStringLiteral("input1"));
// ImCursorRectangle
QVariant variant = view.focusProxy()->inputMethodQuery(Qt::ImCursorRectangle);
QVERIFY(elementGeometry(view.page(), "input1").contains(variant.toRect().topLeft()));
// We assigned the serif font family to be the same as the fixed font family.
// Then test ImFont on a serif styled element, we should get our fixed font family.
variant = view.focusProxy()->inputMethodQuery(Qt::ImFont);
QFont font = variant.value<QFont>();
QEXPECT_FAIL("", "UNIMPLEMENTED: RenderWidgetHostViewQt::inputMethodQuery(Qt::ImFont)", Continue);
QCOMPARE(view.settings()->fontFamily(QWebEngineSettings::FixedFont), font.family());
QList<QInputMethodEvent::Attribute> inputAttributes;
// Insert text
{
QString text = QStringLiteral("QtWebEngine");
QInputMethodEvent eventText(text, inputAttributes);
QApplication::sendEvent(view.focusProxy(), &eventText);
QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.getElementById('input1').value").toString(), text);
QCOMPARE(selectionChangedSpy.count(), 0);
}
{
QString text = QStringLiteral("QtWebEngine");
QInputMethodEvent eventText("", inputAttributes);
eventText.setCommitString(text, 0, 0);
QApplication::sendEvent(view.focusProxy(), &eventText);
QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.getElementById('input1').value").toString(), text);
QCOMPARE(selectionChangedSpy.count(), 0);
}
// ImMaximumTextLength
QEXPECT_FAIL("", "UNIMPLEMENTED: RenderWidgetHostViewQt::inputMethodQuery(Qt::ImMaximumTextLength)", Continue);
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImMaximumTextLength).toInt(), 20);
// Set selection
inputAttributes << QInputMethodEvent::Attribute(QInputMethodEvent::Selection, 3, 2, QVariant());
QInputMethodEvent eventSelection1("", inputAttributes);
QApplication::sendEvent(view.focusProxy(), &eventSelection1);
QTRY_COMPARE(selectionChangedSpy.size(), 1);
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImAnchorPosition).toInt(), 3);
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImCursorPosition).toInt(), 5);
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImCurrentSelection).toString(), QString("eb"));
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImSurroundingText).toString(), QString("QtWebEngine"));
// Set selection with negative length
inputAttributes << QInputMethodEvent::Attribute(QInputMethodEvent::Selection, 6, -5, QVariant());
QInputMethodEvent eventSelection2("", inputAttributes);
QApplication::sendEvent(view.focusProxy(), &eventSelection2);
QTRY_COMPARE(selectionChangedSpy.size(), 2);
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImAnchorPosition).toInt(), 1);
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImCursorPosition).toInt(), 6);
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImCurrentSelection).toString(), QString("tWebE"));
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImSurroundingText).toString(), QString("QtWebEngine"));
QList<QInputMethodEvent::Attribute> attributes;
// Clear the selection, so the next test does not clear any contents.
QInputMethodEvent::Attribute newSelection(QInputMethodEvent::Selection, 0, 0, QVariant());
attributes.append(newSelection);
QInputMethodEvent eventComposition("composition", attributes);
QApplication::sendEvent(view.focusProxy(), &eventComposition);
QTRY_COMPARE(selectionChangedSpy.size(), 3);
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImCurrentSelection).toString(), QString(""));
// An ongoing composition should not change the surrounding text before it is committed.
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImSurroundingText).toString(), QString("QtWebEngine"));
// Cancel current composition first
inputAttributes << QInputMethodEvent::Attribute(QInputMethodEvent::Selection, 0, 0, QVariant());
QInputMethodEvent eventSelection3("", inputAttributes);
QApplication::sendEvent(view.focusProxy(), &eventSelection3);
// Cancelling composition should not clear the surrounding text
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImSurroundingText).toString(), QString("QtWebEngine"));
}
void tst_QWebEngineView::textSelectionInInputField()
{
QWebEngineView view;
view.settings()->setAttribute(QWebEngineSettings::FocusOnNavigationEnabled, true);
view.resize(640, 480);
view.show();
QSignalSpy selectionChangedSpy(&view, SIGNAL(selectionChanged()));
QSignalSpy loadFinishedSpy(&view, SIGNAL(loadFinished(bool)));
view.setHtml("<html><body>"
" <input type='text' id='input1' value='QtWebEngine' size='50'/>"
"</body></html>");
QVERIFY(loadFinishedSpy.wait());
// Tests for Selection when the Editor is NOT in Composition mode
// LEFT to RIGHT selection
// Mouse click event moves the current cursor to the end of the text
QPoint textInputCenter = elementCenter(view.page(), "input1");
QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, textInputCenter);
QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString(), QStringLiteral("input1"));
QTRY_COMPARE(view.focusProxy()->inputMethodQuery(Qt::ImCursorPosition).toInt(), 11);
QTRY_COMPARE(view.focusProxy()->inputMethodQuery(Qt::ImAnchorPosition).toInt(), 11);
// There was no selection to be changed by the click
QCOMPARE(selectionChangedSpy.count(), 0);
QList<QInputMethodEvent::Attribute> attributes;
QInputMethodEvent event(QString(), attributes);
event.setCommitString("XXX", 0, 0);
QApplication::sendEvent(view.focusProxy(), &event);
QTRY_COMPARE(view.focusProxy()->inputMethodQuery(Qt::ImSurroundingText).toString(), QString("QtWebEngineXXX"));
QCOMPARE(selectionChangedSpy.count(), 0);
event.setCommitString(QString(), -2, 2); // Erase two characters.
QApplication::sendEvent(view.focusProxy(), &event);
QTRY_COMPARE(view.focusProxy()->inputMethodQuery(Qt::ImSurroundingText).toString(), QString("QtWebEngineX"));
QCOMPARE(selectionChangedSpy.count(), 0);
event.setCommitString(QString(), -1, 1); // Erase one character.
QApplication::sendEvent(view.focusProxy(), &event);
QTRY_COMPARE(view.focusProxy()->inputMethodQuery(Qt::ImSurroundingText).toString(), QString("QtWebEngine"));
QCOMPARE(selectionChangedSpy.count(), 0);
// Move to the start of the line
QTest::keyClick(view.focusProxy(), Qt::Key_Home);
// Move 2 characters RIGHT
for (int j = 0; j < 2; ++j)
QTest::keyClick(view.focusProxy(), Qt::Key_Right);
// Select to the end of the line
QTest::keyClick(view.focusProxy(), Qt::Key_End, Qt::ShiftModifier);
QVERIFY(selectionChangedSpy.wait());
QCOMPARE(selectionChangedSpy.count(), 1);
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImAnchorPosition).toInt(), 2);
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImCursorPosition).toInt(), 11);
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImCurrentSelection).toString(), QString("WebEngine"));
// RIGHT to LEFT selection
// Deselect the selection (this moves the current cursor to the end of the text)
QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, textInputCenter);
QVERIFY(selectionChangedSpy.wait());
QCOMPARE(selectionChangedSpy.count(), 2);
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImAnchorPosition).toInt(), 11);
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImCursorPosition).toInt(), 11);
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImCurrentSelection).toString(), QString(""));
// Move 2 characters LEFT
for (int i = 0; i < 2; ++i)
QTest::keyClick(view.focusProxy(), Qt::Key_Left);
// Select to the start of the line
QTest::keyClick(view.focusProxy(), Qt::Key_Home, Qt::ShiftModifier);
QVERIFY(selectionChangedSpy.wait());
QCOMPARE(selectionChangedSpy.count(), 3);
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImAnchorPosition).toInt(), 9);
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImCursorPosition).toInt(), 0);
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImCurrentSelection).toString(), QString("QtWebEngi"));
}
void tst_QWebEngineView::textSelectionOutOfInputField()
{
QWebEngineView view;
view.resize(640, 480);
view.show();
QSignalSpy selectionChangedSpy(&view, SIGNAL(selectionChanged()));
QSignalSpy loadFinishedSpy(&view, SIGNAL(loadFinished(bool)));
view.setHtml("<html><body>"
" This is a text"
"</body></html>");
QVERIFY(loadFinishedSpy.wait());
QCOMPARE(selectionChangedSpy.count(), 0);
QVERIFY(!view.hasSelection());
QVERIFY(view.page()->selectedText().isEmpty());
// Simple click should not update text selection, however it updates selection bounds in Chromium
QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, view.geometry().center());
QCOMPARE(selectionChangedSpy.count(), 0);
QVERIFY(!view.hasSelection());
QVERIFY(view.page()->selectedText().isEmpty());
// Select text by ctrl+a
QTest::keyClick(view.windowHandle(), Qt::Key_A, Qt::ControlModifier);
QVERIFY(selectionChangedSpy.wait());
QCOMPARE(selectionChangedSpy.count(), 1);
QVERIFY(view.hasSelection());
QCOMPARE(view.page()->selectedText(), QString("This is a text"));
// Deselect text by mouse click
QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, view.geometry().center());
QVERIFY(selectionChangedSpy.wait());
QCOMPARE(selectionChangedSpy.count(), 2);
QVERIFY(!view.hasSelection());
QVERIFY(view.page()->selectedText().isEmpty());
// Select text by ctrl+a
QTest::keyClick(view.windowHandle(), Qt::Key_A, Qt::ControlModifier);
QVERIFY(selectionChangedSpy.wait());
QCOMPARE(selectionChangedSpy.count(), 3);
QVERIFY(view.hasSelection());
QCOMPARE(view.page()->selectedText(), QString("This is a text"));
// Deselect text via discard+undiscard
view.hide();
view.page()->setLifecycleState(QWebEnginePage::LifecycleState::Discarded);
view.show();
QVERIFY(loadFinishedSpy.wait());
QCOMPARE(selectionChangedSpy.count(), 4);
QVERIFY(!view.hasSelection());
QVERIFY(view.page()->selectedText().isEmpty());
selectionChangedSpy.clear();
view.setHtml("<html><body>"
" This is a text"
" <br>"
" <input type='text' id='input1' value='QtWebEngine' size='50'/>"
"</body></html>");
QVERIFY(loadFinishedSpy.wait());
QCOMPARE(selectionChangedSpy.count(), 0);
QVERIFY(!view.hasSelection());
QVERIFY(view.page()->selectedText().isEmpty());
// Make sure the input field does not have the focus
evaluateJavaScriptSync(view.page(), "document.getElementById('input1').blur()");
QTRY_VERIFY(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString().isEmpty());
// Select the whole page by ctrl+a
QTest::keyClick(view.windowHandle(), Qt::Key_A, Qt::ControlModifier);
QVERIFY(selectionChangedSpy.wait());
QCOMPARE(selectionChangedSpy.count(), 1);
QVERIFY(view.hasSelection());
QVERIFY(view.page()->selectedText().startsWith(QString("This is a text")));
// Remove selection by clicking into an input field
QPoint textInputCenter = elementCenter(view.page(), "input1");
QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, textInputCenter);
QVERIFY(selectionChangedSpy.wait());
QCOMPARE(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString(), QStringLiteral("input1"));
QCOMPARE(selectionChangedSpy.count(), 2);
QVERIFY(!view.hasSelection());
QVERIFY(view.page()->selectedText().isEmpty());
// Select the content of the input field by ctrl+a
QTest::keyClick(view.windowHandle(), Qt::Key_A, Qt::ControlModifier);
QVERIFY(selectionChangedSpy.wait());
QCOMPARE(selectionChangedSpy.count(), 3);
QVERIFY(view.hasSelection());
QCOMPARE(view.page()->selectedText(), QString("QtWebEngine"));
// Deselect input field's text by mouse click
QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, view.geometry().center());
QVERIFY(selectionChangedSpy.wait());
QCOMPARE(selectionChangedSpy.count(), 4);
QVERIFY(!view.hasSelection());
QVERIFY(view.page()->selectedText().isEmpty());
}
void tst_QWebEngineView::hiddenText()
{
QWebEngineView view;
view.resize(640, 480);
view.show();
QSignalSpy loadFinishedSpy(&view, SIGNAL(loadFinished(bool)));
view.setHtml("<html><body>"
" <input type='text' id='input1' value='QtWebEngine' size='50'/><br>"
" <input type='password' id='password1'/>"
"</body></html>");
QVERIFY(loadFinishedSpy.wait());
QPoint passwordInputCenter = elementCenter(view.page(), "password1");
QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, passwordInputCenter);
QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString(), QStringLiteral("password1"));
QVERIFY(!view.focusProxy()->testAttribute(Qt::WA_InputMethodEnabled));
QVERIFY(view.focusProxy()->inputMethodHints() & Qt::ImhHiddenText);
QPoint textInputCenter = elementCenter(view.page(), "input1");
QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, textInputCenter);
QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString(), QStringLiteral("input1"));
QVERIFY(!(view.focusProxy()->inputMethodHints() & Qt::ImhHiddenText));
}
void tst_QWebEngineView::emptyInputMethodEvent()
{
QWebEngineView view;
view.settings()->setAttribute(QWebEngineSettings::FocusOnNavigationEnabled, true);
view.resize(640, 480);
view.show();
QSignalSpy selectionChangedSpy(&view, SIGNAL(selectionChanged()));
QSignalSpy loadFinishedSpy(&view, SIGNAL(loadFinished(bool)));
view.setHtml("<html><body>"
" <input type='text' id='input1' value='QtWebEngine'/>"
"</body></html>");
QVERIFY(loadFinishedSpy.wait());
evaluateJavaScriptSync(view.page(), "var inputEle = document.getElementById('input1'); inputEle.focus(); inputEle.select();");
QTRY_COMPARE(selectionChangedSpy.count(), 1);
// 1. Empty input method event does not clear text
QInputMethodEvent emptyEvent;
QVERIFY(QApplication::sendEvent(view.focusProxy(), &emptyEvent));
qApp->processEvents();
QCOMPARE(evaluateJavaScriptSync(view.page(), "document.getElementById('input1').value").toString(), QStringLiteral("QtWebEngine"));
QTRY_COMPARE(view.focusProxy()->inputMethodQuery(Qt::ImSurroundingText).toString(), QStringLiteral("QtWebEngine"));
// Reset: clear input field
evaluateJavaScriptSync(view.page(), "var inputEle = document.getElementById('input1').value = ''");
QTRY_VERIFY(evaluateJavaScriptSync(view.page(), "document.getElementById('input1').value").toString().isEmpty());
QTRY_VERIFY(view.focusProxy()->inputMethodQuery(Qt::ImSurroundingText).toString().isEmpty());
// 2. Cancel IME composition with empty input method event
// Start IME composition
QList<QInputMethodEvent::Attribute> attributes;
QInputMethodEvent eventComposition("a", attributes);
QVERIFY(QApplication::sendEvent(view.focusProxy(), &eventComposition));
QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.getElementById('input1').value").toString(), QStringLiteral("a"));
QTRY_VERIFY(view.focusProxy()->inputMethodQuery(Qt::ImSurroundingText).toString().isEmpty());
// Cancel IME composition
QVERIFY(QApplication::sendEvent(view.focusProxy(), &emptyEvent));
QTRY_VERIFY(evaluateJavaScriptSync(view.page(), "document.getElementById('input1').value").toString().isEmpty());
QTRY_VERIFY(view.focusProxy()->inputMethodQuery(Qt::ImSurroundingText).toString().isEmpty());
// Try key press after cancelled IME composition
QTest::keyClick(view.focusProxy(), Qt::Key_B);
QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.getElementById('input1').value").toString(), QStringLiteral("b"));
QTRY_COMPARE(view.focusProxy()->inputMethodQuery(Qt::ImSurroundingText).toString(), QStringLiteral("b"));
}
void tst_QWebEngineView::imeComposition()
{
QWebEngineView view;
view.settings()->setAttribute(QWebEngineSettings::FocusOnNavigationEnabled, true);
view.resize(640, 480);
view.show();
QSignalSpy selectionChangedSpy(&view, SIGNAL(selectionChanged()));
QSignalSpy loadFinishedSpy(&view, SIGNAL(loadFinished(bool)));
view.setHtml("<html><body>"
" <input type='text' id='input1' value='QtWebEngine inputMethod'/>"
"</body></html>");
QVERIFY(loadFinishedSpy.wait());
evaluateJavaScriptSync(view.page(), "var inputEle = document.getElementById('input1'); inputEle.focus(); inputEle.select();");
QTRY_COMPARE(selectionChangedSpy.count(), 1);
// Clear the selection, also cancel the ongoing composition if there is one.
{
QList<QInputMethodEvent::Attribute> attributes;
QInputMethodEvent::Attribute newSelection(QInputMethodEvent::Selection, 0, 0, QVariant());
attributes.append(newSelection);
QInputMethodEvent event("", attributes);
QApplication::sendEvent(view.focusProxy(), &event);
selectionChangedSpy.wait();
QCOMPARE(selectionChangedSpy.count(), 2);
}
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImSurroundingText).toString(), QString("QtWebEngine inputMethod"));
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImAnchorPosition).toInt(), 0);
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImCursorPosition).toInt(), 0);
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImCurrentSelection).toString(), QString(""));
selectionChangedSpy.clear();
// 1. Insert a character to the beginning of the line.
// Send temporary text, which makes the editor has composition 'm'.
{
QList<QInputMethodEvent::Attribute> attributes;
QInputMethodEvent event("m", attributes);
QApplication::sendEvent(view.focusProxy(), &event);
}
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImSurroundingText).toString(), QString("QtWebEngine inputMethod"));
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImCursorPosition).toInt(), 0);
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImAnchorPosition).toInt(), 0);
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImCurrentSelection).toString(), QString(""));
QCOMPARE(selectionChangedSpy.count(), 0);
// Send temporary text, which makes the editor has composition 'n'.
{
QList<QInputMethodEvent::Attribute> attributes;
QInputMethodEvent event("n", attributes);
QApplication::sendEvent(view.focusProxy(), &event);
}
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImSurroundingText).toString(), QString("QtWebEngine inputMethod"));
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImCursorPosition).toInt(), 0);
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImAnchorPosition).toInt(), 0);
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImCurrentSelection).toString(), QString(""));
QCOMPARE(selectionChangedSpy.count(), 0);
// Send commit text, which makes the editor conforms composition.
{
QList<QInputMethodEvent::Attribute> attributes;
QInputMethodEvent event("", attributes);
event.setCommitString("o");
QApplication::sendEvent(view.focusProxy(), &event);
}
QTRY_COMPARE(view.focusProxy()->inputMethodQuery(Qt::ImSurroundingText).toString(), QString("oQtWebEngine inputMethod"));
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImCursorPosition).toInt(), 1);
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImAnchorPosition).toInt(), 1);
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImCurrentSelection).toString(), QString(""));
QCOMPARE(selectionChangedSpy.count(), 0);
// 2. insert a character to the middle of the line.
// Send temporary text, which makes the editor has composition 'd'.
{
QList<QInputMethodEvent::Attribute> attributes;
QInputMethodEvent event("d", attributes);
QApplication::sendEvent(view.focusProxy(), &event);
}
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImSurroundingText).toString(), QString("oQtWebEngine inputMethod"));
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImCursorPosition).toInt(), 1);
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImAnchorPosition).toInt(), 1);
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImCurrentSelection).toString(), QString(""));
QCOMPARE(selectionChangedSpy.count(), 0);
// Send commit text, which makes the editor conforms composition.
{
QList<QInputMethodEvent::Attribute> attributes;
QInputMethodEvent event("", attributes);
event.setCommitString("e");
QApplication::sendEvent(view.focusProxy(), &event);
}
QTRY_COMPARE(view.focusProxy()->inputMethodQuery(Qt::ImSurroundingText).toString(), QString("oeQtWebEngine inputMethod"));
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImCursorPosition).toInt(), 2);
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImAnchorPosition).toInt(), 2);
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImCurrentSelection).toString(), QString(""));
QCOMPARE(selectionChangedSpy.count(), 0);
// 3. Insert a character to the end of the line.
QTest::keyClick(view.focusProxy(), Qt::Key_End);
QTRY_COMPARE(view.focusProxy()->inputMethodQuery(Qt::ImCursorPosition).toInt(), 25);
QTRY_COMPARE(view.focusProxy()->inputMethodQuery(Qt::ImAnchorPosition).toInt(), 25);
// Send temporary text, which makes the editor has composition 't'.
{
QList<QInputMethodEvent::Attribute> attributes;
QInputMethodEvent event("t", attributes);
QApplication::sendEvent(view.focusProxy(), &event);
}
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImSurroundingText).toString(), QString("oeQtWebEngine inputMethod"));
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImCursorPosition).toInt(), 25);
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImAnchorPosition).toInt(), 25);
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImCurrentSelection).toString(), QString(""));
QCOMPARE(selectionChangedSpy.count(), 0);
// Send commit text, which makes the editor conforms composition.
{
QList<QInputMethodEvent::Attribute> attributes;
QInputMethodEvent event("", attributes);
event.setCommitString("t");
QApplication::sendEvent(view.focusProxy(), &event);
}
QTRY_COMPARE(view.focusProxy()->inputMethodQuery(Qt::ImSurroundingText).toString(), QString("oeQtWebEngine inputMethodt"));
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImCursorPosition).toInt(), 26);
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImAnchorPosition).toInt(), 26);
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImCurrentSelection).toString(), QString(""));
QCOMPARE(selectionChangedSpy.count(), 0);
// 4. Replace the selection.
#ifndef Q_OS_MACOS
QTest::keyClick(view.focusProxy(), Qt::Key_Left, Qt::ShiftModifier | Qt::ControlModifier);
#else
QTest::keyClick(view.focusProxy(), Qt::Key_Left, Qt::ShiftModifier | Qt::AltModifier);
#endif
QVERIFY(selectionChangedSpy.wait());
QCOMPARE(selectionChangedSpy.count(), 1);
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImSurroundingText).toString(), QString("oeQtWebEngine inputMethodt"));
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImCursorPosition).toInt(), 14);
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImAnchorPosition).toInt(), 26);
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImCurrentSelection).toString(), QString("inputMethodt"));
// Send temporary text, which makes the editor has composition 'w'.
{
QList<QInputMethodEvent::Attribute> attributes;
QInputMethodEvent event("w", attributes);
QApplication::sendEvent(view.focusProxy(), &event);
// The new composition should clear the previous selection
QVERIFY(selectionChangedSpy.wait());
QCOMPARE(selectionChangedSpy.count(), 2);
}
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImSurroundingText).toString(), QString("oeQtWebEngine "));
// The cursor should be positioned at the end of the composition text
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImCursorPosition).toInt(), 15);
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImAnchorPosition).toInt(), 15);
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImCurrentSelection).toString(), QString(""));
// Send commit text, which makes the editor conforms composition.
{
QList<QInputMethodEvent::Attribute> attributes;
QInputMethodEvent event("", attributes);
event.setCommitString("2");
QApplication::sendEvent(view.focusProxy(), &event);
}
// There is no text selection to be changed at this point thus we can't wait for selectionChanged signal.
QTRY_COMPARE(view.focusProxy()->inputMethodQuery(Qt::ImSurroundingText).toString(), QString("oeQtWebEngine 2"));
QTRY_COMPARE(view.focusProxy()->inputMethodQuery(Qt::ImCursorPosition).toInt(), 15);
QTRY_COMPARE(view.focusProxy()->inputMethodQuery(Qt::ImAnchorPosition).toInt(), 15);
QTRY_COMPARE(view.focusProxy()->inputMethodQuery(Qt::ImCurrentSelection).toString(), QString(""));
QCOMPARE(selectionChangedSpy.count(), 2);
selectionChangedSpy.clear();
// 5. Mimic behavior of QtVirtualKeyboard with enabled text prediction.
evaluateJavaScriptSync(view.page(), "document.getElementById('input1').value='QtWebEngine';");
QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.getElementById('input1').value").toString(), QString("QtWebEngine"));
// Move cursor into position.
QTest::keyClick(view.focusProxy(), Qt::Key_Home);
for (int j = 0; j < 2; ++j)
QTest::keyClick(view.focusProxy(), Qt::Key_Right);
QTRY_COMPARE(view.focusProxy()->inputMethodQuery(Qt::ImCursorPosition).toInt(), 2);
// Turn text into composition by using negative start position.
{
int replaceFrom = -1 * view.focusProxy()->inputMethodQuery(Qt::ImCursorPosition).toInt();
int replaceLength = view.focusProxy()->inputMethodQuery(Qt::ImSurroundingText).toString().size();
QList<QInputMethodEvent::Attribute> attributes;
QInputMethodEvent event("QtWebEngine", attributes);
event.setCommitString(QString(), replaceFrom, replaceLength);
QApplication::sendEvent(view.focusProxy(), &event);
}
QTRY_COMPARE(view.focusProxy()->inputMethodQuery(Qt::ImSurroundingText).toString(), QString(""));
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImCursorPosition).toInt(), 11);
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImAnchorPosition).toInt(), 11);
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImCurrentSelection).toString(), QString(""));
QCOMPARE(evaluateJavaScriptSync(view.page(), "document.getElementById('input1').value").toString(), QString("QtWebEngine"));
// Commit.
{
QList<QInputMethodEvent::Attribute> attributes;
QInputMethodEvent event(QString(), attributes);
event.setCommitString("QtWebEngine", 0, 0);
QApplication::sendEvent(view.focusProxy(), &event);
}
QTRY_COMPARE(view.focusProxy()->inputMethodQuery(Qt::ImSurroundingText).toString(), QString("QtWebEngine"));
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImCursorPosition).toInt(), 11);
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImAnchorPosition).toInt(), 11);
QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImCurrentSelection).toString(), QString(""));
QCOMPARE(evaluateJavaScriptSync(view.page(), "document.getElementById('input1').value").toString(), QString("QtWebEngine"));
QCOMPARE(selectionChangedSpy.count(), 0);
}
void tst_QWebEngineView::newlineInTextarea()
{
QWebEngineView view;
view.settings()->setAttribute(QWebEngineSettings::FocusOnNavigationEnabled, true);
view.resize(640, 480);
view.show();
QSignalSpy loadFinishedSpy(&view, SIGNAL(loadFinished(bool)));
view.page()->setHtml("<html><body>"
" <textarea rows='5' cols='1' id='input1'></textarea>"
"</body></html>");
QVERIFY(loadFinishedSpy.wait());
evaluateJavaScriptSync(view.page(), "var inputEle = document.getElementById('input1'); inputEle.focus(); inputEle.select();");
QTRY_VERIFY(evaluateJavaScriptSync(view.page(), "document.getElementById('input1').value").toString().isEmpty());
// Enter Key without key text
QKeyEvent keyPressEnter(QEvent::KeyPress, Qt::Key_Enter, Qt::NoModifier);
QKeyEvent keyReleaseEnter(QEvent::KeyRelease, Qt::Key_Enter, Qt::NoModifier);
QApplication::sendEvent(view.focusProxy(), &keyPressEnter);
QApplication::sendEvent(view.focusProxy(), &keyReleaseEnter);
QList<QInputMethodEvent::Attribute> attribs;
QInputMethodEvent eventText(QString(), attribs);
eventText.setCommitString("\n");
QApplication::sendEvent(view.focusProxy(), &eventText);
QInputMethodEvent eventText2(QString(), attribs);
eventText2.setCommitString("third line");
QApplication::sendEvent(view.focusProxy(), &eventText2);
qApp->processEvents();
QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.getElementById('input1').value").toString(), QString("\n\nthird line"));
QTRY_COMPARE(view.focusProxy()->inputMethodQuery(Qt::ImSurroundingText).toString(), QString("\n\nthird line"));
// Enter Key with key text '\r'
evaluateJavaScriptSync(view.page(), "var inputEle = document.getElementById('input1'); inputEle.value = ''; inputEle.focus(); inputEle.select();");
QTRY_VERIFY(evaluateJavaScriptSync(view.page(), "document.getElementById('input1').value").toString().isEmpty());
QKeyEvent keyPressEnterWithCarriageReturn(QEvent::KeyPress, Qt::Key_Enter, Qt::NoModifier, "\r");
QKeyEvent keyReleaseEnterWithCarriageReturn(QEvent::KeyRelease, Qt::Key_Enter, Qt::NoModifier);
QApplication::sendEvent(view.focusProxy(), &keyPressEnterWithCarriageReturn);
QApplication::sendEvent(view.focusProxy(), &keyReleaseEnterWithCarriageReturn);
QApplication::sendEvent(view.focusProxy(), &eventText);
QApplication::sendEvent(view.focusProxy(), &eventText2);
qApp->processEvents();
QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.getElementById('input1').value").toString(), QString("\n\nthird line"));
QTRY_COMPARE(view.focusProxy()->inputMethodQuery(Qt::ImSurroundingText).toString(), QString("\n\nthird line"));
// Enter Key with key text '\n'
evaluateJavaScriptSync(view.page(), "var inputEle = document.getElementById('input1'); inputEle.value = ''; inputEle.focus(); inputEle.select();");
QTRY_VERIFY(evaluateJavaScriptSync(view.page(), "document.getElementById('input1').value").toString().isEmpty());
QKeyEvent keyPressEnterWithLineFeed(QEvent::KeyPress, Qt::Key_Enter, Qt::NoModifier, "\n");
QKeyEvent keyReleaseEnterWithLineFeed(QEvent::KeyRelease, Qt::Key_Enter, Qt::NoModifier, "\n");
QApplication::sendEvent(view.focusProxy(), &keyPressEnterWithLineFeed);
QApplication::sendEvent(view.focusProxy(), &keyReleaseEnterWithLineFeed);
QApplication::sendEvent(view.focusProxy(), &eventText);
QApplication::sendEvent(view.focusProxy(), &eventText2);
qApp->processEvents();
QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.getElementById('input1').value").toString(), QString("\n\nthird line"));
QTRY_COMPARE(view.focusProxy()->inputMethodQuery(Qt::ImSurroundingText).toString(), QString("\n\nthird line"));
// Enter Key with key text "\n\r"
evaluateJavaScriptSync(view.page(), "var inputEle = document.getElementById('input1'); inputEle.value = ''; inputEle.focus(); inputEle.select();");
QTRY_VERIFY(evaluateJavaScriptSync(view.page(), "document.getElementById('input1').value").toString().isEmpty());
QKeyEvent keyPressEnterWithLFCR(QEvent::KeyPress, Qt::Key_Enter, Qt::NoModifier, "\n\r");
QKeyEvent keyReleaseEnterWithLFCR(QEvent::KeyRelease, Qt::Key_Enter, Qt::NoModifier, "\n\r");
QApplication::sendEvent(view.focusProxy(), &keyPressEnterWithLFCR);
QApplication::sendEvent(view.focusProxy(), &keyReleaseEnterWithLFCR);
QApplication::sendEvent(view.focusProxy(), &eventText);
QApplication::sendEvent(view.focusProxy(), &eventText2);
qApp->processEvents();
QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.getElementById('input1').value").toString(), QString("\n\nthird line"));
QTRY_COMPARE(view.focusProxy()->inputMethodQuery(Qt::ImSurroundingText).toString(), QString("\n\nthird line"));
// Return Key without key text
evaluateJavaScriptSync(view.page(), "var inputEle = document.getElementById('input1'); inputEle.value = ''; inputEle.focus(); inputEle.select();");
QTRY_VERIFY(evaluateJavaScriptSync(view.page(), "document.getElementById('input1').value").toString().isEmpty());
QKeyEvent keyPressReturn(QEvent::KeyPress, Qt::Key_Enter, Qt::NoModifier);
QKeyEvent keyReleaseReturn(QEvent::KeyRelease, Qt::Key_Enter, Qt::NoModifier);
QApplication::sendEvent(view.focusProxy(), &keyPressReturn);
QApplication::sendEvent(view.focusProxy(), &keyReleaseReturn);
QApplication::sendEvent(view.focusProxy(), &eventText);
QApplication::sendEvent(view.focusProxy(), &eventText2);
qApp->processEvents();
QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.getElementById('input1').value").toString(), QString("\n\nthird line"));
QTRY_COMPARE(view.focusProxy()->inputMethodQuery(Qt::ImSurroundingText).toString(), QString("\n\nthird line"));
}
void tst_QWebEngineView::imeJSInputEvents()
{
QWebEngineView view;
view.resize(640, 480);
view.settings()->setAttribute(QWebEngineSettings::FocusOnNavigationEnabled, true);
view.show();
auto logLines = [&view]() -> QStringList {
return evaluateJavaScriptSync(view.page(), "log.textContent").toString().split("\n").filter(QRegularExpression(".+"));
};
QSignalSpy loadFinishedSpy(&view, SIGNAL(loadFinished(bool)));
view.page()->setHtml("<html>"
"<head><script>"
" var input, log;"
" function verboseEvent(ev) {"
" log.textContent += ev + ' ' + ev.type + ' ' + ev.data + '\\n';"
" }"
" function clear(ev) {"
" log.textContent = '';"
" input.textContent = '';"
" }"
" function init() {"
" input = document.getElementById('input');"
" log = document.getElementById('log');"
" events = [ 'textInput', 'beforeinput', 'input', 'compositionstart', 'compositionupdate', 'compositionend' ];"
" for (var e in events)"
" input.addEventListener(events[e], verboseEvent);"
" }"
"</script></head>"
"<body onload='init()'>"
" <div id='input' contenteditable='true' style='border-style: solid;'></div>"
" <pre id='log'></pre>"
"</body></html>");
QVERIFY(loadFinishedSpy.wait());
evaluateJavaScriptSync(view.page(), "document.getElementById('input').focus()");
QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString(), QStringLiteral("input"));
// 1. Commit text (this is how dead keys work on Linux).
{
QList<QInputMethodEvent::Attribute> attributes;
QInputMethodEvent event("", attributes);
event.setCommitString("commit");
QApplication::sendEvent(view.focusProxy(), &event);
qApp->processEvents();
}
// Simply committing text should not trigger any JS composition event.
QTRY_COMPARE(logLines().count(), 3);
QCOMPARE(logLines()[0], QStringLiteral("[object InputEvent] beforeinput commit"));
QCOMPARE(logLines()[1], QStringLiteral("[object TextEvent] textInput commit"));
QCOMPARE(logLines()[2], QStringLiteral("[object InputEvent] input commit"));
evaluateJavaScriptSync(view.page(), "clear()");
QTRY_VERIFY(evaluateJavaScriptSync(view.page(), "log.textContent + input.textContent").toString().isEmpty());
// 2. Start composition then commit text (this is how dead keys work on macOS).
{
QList<QInputMethodEvent::Attribute> attributes;
QInputMethodEvent event("preedit", attributes);
QApplication::sendEvent(view.focusProxy(), &event);
qApp->processEvents();
}
QTRY_COMPARE(logLines().count(), 4);
QCOMPARE(logLines()[0], QStringLiteral("[object CompositionEvent] compositionstart "));
QCOMPARE(logLines()[1], QStringLiteral("[object InputEvent] beforeinput preedit"));
QCOMPARE(logLines()[2], QStringLiteral("[object CompositionEvent] compositionupdate preedit"));
QCOMPARE(logLines()[3], QStringLiteral("[object InputEvent] input preedit"));
{
QList<QInputMethodEvent::Attribute> attributes;
QInputMethodEvent event("", attributes);
event.setCommitString("commit");
QApplication::sendEvent(view.focusProxy(), &event);
qApp->processEvents();
}
QTRY_COMPARE(logLines().count(), 9);
QCOMPARE(logLines()[4], QStringLiteral("[object InputEvent] beforeinput commit"));
QCOMPARE(logLines()[5], QStringLiteral("[object CompositionEvent] compositionupdate commit"));
QCOMPARE(logLines()[6], QStringLiteral("[object TextEvent] textInput commit"));
QCOMPARE(logLines()[7], QStringLiteral("[object InputEvent] input commit"));
QCOMPARE(logLines()[8], QStringLiteral("[object CompositionEvent] compositionend commit"));
evaluateJavaScriptSync(view.page(), "clear()");
QTRY_VERIFY(evaluateJavaScriptSync(view.page(), "log.textContent + input.textContent").toString().isEmpty());
// 3. Start composition then cancel it with an empty IME event.
{
QList<QInputMethodEvent::Attribute> attributes;
QInputMethodEvent event("preedit", attributes);
QApplication::sendEvent(view.focusProxy(), &event);
qApp->processEvents();
}
QTRY_COMPARE(logLines().count(), 4);
QCOMPARE(logLines()[0], QStringLiteral("[object CompositionEvent] compositionstart "));
QCOMPARE(logLines()[1], QStringLiteral("[object InputEvent] beforeinput preedit"));
QCOMPARE(logLines()[2], QStringLiteral("[object CompositionEvent] compositionupdate preedit"));
QCOMPARE(logLines()[3], QStringLiteral("[object InputEvent] input preedit"));
{
QList<QInputMethodEvent::Attribute> attributes;
QInputMethodEvent event("", attributes);
QApplication::sendEvent(view.focusProxy(), &event);
qApp->processEvents();
}
QTRY_COMPARE(logLines().count(), 9);
QCOMPARE(logLines()[4], QStringLiteral("[object InputEvent] beforeinput "));
QCOMPARE(logLines()[5], QStringLiteral("[object CompositionEvent] compositionupdate "));
QCOMPARE(logLines()[6], QStringLiteral("[object TextEvent] textInput "));
QCOMPARE(logLines()[7], QStringLiteral("[object InputEvent] input null"));
QCOMPARE(logLines()[8], QStringLiteral("[object CompositionEvent] compositionend "));
evaluateJavaScriptSync(view.page(), "clear()");
QTRY_VERIFY(evaluateJavaScriptSync(view.page(), "log.textContent + input.textContent").toString().isEmpty());
// 4. Send empty IME event.
{
QList<QInputMethodEvent::Attribute> attributes;
QInputMethodEvent event("", attributes);
QApplication::sendEvent(view.focusProxy(), &event);
qApp->processEvents();
}
// No JS event is expected.
QTest::qWait(100);
QVERIFY(logLines().isEmpty());
evaluateJavaScriptSync(view.page(), "clear()");
QTRY_VERIFY(evaluateJavaScriptSync(view.page(), "log.textContent + input.textContent").toString().isEmpty());
}
void tst_QWebEngineView::imeCompositionQueryEvent_data()
{
QTest::addColumn<QString>("receiverObjectName");
QTest::newRow("focusObject") << QString("focusObject");
QTest::newRow("focusProxy") << QString("focusProxy");
QTest::newRow("focusWidget") << QString("focusWidget");
}
void tst_QWebEngineView::imeCompositionQueryEvent()
{
QWebEngineView view;
view.resize(640, 480);
view.settings()->setAttribute(QWebEngineSettings::FocusOnNavigationEnabled, true);
view.show();
QSignalSpy loadFinishedSpy(&view, SIGNAL(loadFinished(bool)));
view.setHtml("<html><body>"
" <input type='text' id='input1' />"
"</body></html>");
QVERIFY(loadFinishedSpy.wait());
evaluateJavaScriptSync(view.page(), "document.getElementById('input1').focus()");
QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString(), QStringLiteral("input1"));
QObject *input = nullptr;
QFETCH(QString, receiverObjectName);
if (receiverObjectName == "focusObject") {
QTRY_VERIFY(qApp->focusObject());
input = qApp->focusObject();
} else if (receiverObjectName == "focusProxy") {
QTRY_VERIFY(view.focusProxy());
input = view.focusProxy();
} else if (receiverObjectName == "focusWidget") {
QTRY_VERIFY(view.focusWidget());
input = view.focusWidget();
}
QInputMethodQueryEvent srrndTextQuery(Qt::ImSurroundingText);
QInputMethodQueryEvent cursorPosQuery(Qt::ImCursorPosition);
QInputMethodQueryEvent anchorPosQuery(Qt::ImAnchorPosition);
// Set composition
{
QList<QInputMethodEvent::Attribute> attributes;
QInputMethodEvent event("composition", attributes);
QApplication::sendEvent(input, &event);
qApp->processEvents();
}
QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.getElementById('input1').value").toString(), QString("composition"));
QTRY_COMPARE(view.focusProxy()->inputMethodQuery(Qt::ImCursorPosition).toInt(), 11);
QApplication::sendEvent(input, &srrndTextQuery);
QApplication::sendEvent(input, &cursorPosQuery);
QApplication::sendEvent(input, &anchorPosQuery);
qApp->processEvents();
QTRY_COMPARE(srrndTextQuery.value(Qt::ImSurroundingText).toString(), QString(""));
QTRY_COMPARE(cursorPosQuery.value(Qt::ImCursorPosition).toInt(), 11);
QTRY_COMPARE(anchorPosQuery.value(Qt::ImAnchorPosition).toInt(), 11);
// Send commit
{
QList<QInputMethodEvent::Attribute> attributes;
QInputMethodEvent event("", attributes);
event.setCommitString("composition");
QApplication::sendEvent(input, &event);
qApp->processEvents();
}
QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.getElementById('input1').value").toString(), QString("composition"));
QTRY_COMPARE(view.focusProxy()->inputMethodQuery(Qt::ImSurroundingText).toString(), QString("composition"));
QApplication::sendEvent(input, &srrndTextQuery);
QApplication::sendEvent(input, &cursorPosQuery);
QApplication::sendEvent(input, &anchorPosQuery);
qApp->processEvents();
QTRY_COMPARE(srrndTextQuery.value(Qt::ImSurroundingText).toString(), QString("composition"));
QTRY_COMPARE(cursorPosQuery.value(Qt::ImCursorPosition).toInt(), 11);
QTRY_COMPARE(anchorPosQuery.value(Qt::ImAnchorPosition).toInt(), 11);
}
#ifndef QT_NO_CLIPBOARD
void tst_QWebEngineView::globalMouseSelection()
{
if (!QApplication::clipboard()->supportsSelection()) {
QSKIP("Test only relevant for systems with selection");
return;
}
QApplication::clipboard()->clear(QClipboard::Selection);
QWebEngineView view;
view.resize(640, 480);
view.show();
QSignalSpy selectionChangedSpy(&view, SIGNAL(selectionChanged()));
QSignalSpy loadFinishedSpy(&view, SIGNAL(loadFinished(bool)));
view.setHtml("<html><body>"
" <input type='text' id='input1' value='QtWebEngine' size='50' />"
"</body></html>");
QVERIFY(loadFinishedSpy.wait());
// Select text via JavaScript
evaluateJavaScriptSync(view.page(), "var inputEle = document.getElementById('input1'); inputEle.focus(); inputEle.select();");
QTRY_COMPARE(selectionChangedSpy.count(), 1);
QVERIFY(QApplication::clipboard()->text(QClipboard::Selection).isEmpty());
// Deselect the selection (this moves the current cursor to the end of the text)
QPoint textInputCenter = elementCenter(view.page(), "input1");
QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, textInputCenter);
QVERIFY(selectionChangedSpy.wait());
QCOMPARE(selectionChangedSpy.count(), 2);
QVERIFY(QApplication::clipboard()->text(QClipboard::Selection).isEmpty());
// Select to the start of the line
QTest::keyClick(view.focusProxy(), Qt::Key_Home, Qt::ShiftModifier);
QVERIFY(selectionChangedSpy.wait());
QCOMPARE(selectionChangedSpy.count(), 3);
QCOMPARE(QApplication::clipboard()->text(QClipboard::Selection), QStringLiteral("QtWebEngine"));
}
#endif
void tst_QWebEngineView::noContextMenu()
{
QWidget wrapper;
wrapper.setContextMenuPolicy(Qt::CustomContextMenu);
connect(&wrapper, &QWidget::customContextMenuRequested, [&wrapper](const QPoint &pt) {
QMenu* menu = new QMenu(&wrapper);
menu->addAction("Action1");
menu->addAction("Action2");
menu->popup(pt);
});
QWebEngineView view(&wrapper);
view.setContextMenuPolicy(Qt::NoContextMenu);
wrapper.show();
QVERIFY(view.findChildren<QMenu *>().isEmpty());
QVERIFY(wrapper.findChildren<QMenu *>().isEmpty());
QTest::mouseMove(wrapper.windowHandle(), QPoint(10,10));
QTest::mouseClick(wrapper.windowHandle(), Qt::RightButton);
QTRY_COMPARE(wrapper.findChildren<QMenu *>().count(), 1);
QVERIFY(view.findChildren<QMenu *>().isEmpty());
}
void tst_QWebEngineView::contextMenu_data()
{
QTest::addColumn<int>("childrenCount");
QTest::addColumn<bool>("isCustomMenu");
QTest::addColumn<Qt::ContextMenuPolicy>("contextMenuPolicy");
QTest::newRow("defaultContextMenu") << 1 << false << Qt::DefaultContextMenu;
QTest::newRow("customContextMenu") << 1 << true << Qt::CustomContextMenu;
QTest::newRow("preventContextMenu") << 0 << false << Qt::PreventContextMenu;
}
void tst_QWebEngineView::contextMenu()
{
QFETCH(int, childrenCount);
QFETCH(bool, isCustomMenu);
QFETCH(Qt::ContextMenuPolicy, contextMenuPolicy);
QWebEngineView view;
QMenu *customMenu = nullptr;
if (contextMenuPolicy == Qt::CustomContextMenu) {
connect(&view, &QWebEngineView::customContextMenuRequested, [&view, &customMenu] (const QPoint &pt) {
Q_ASSERT(!customMenu);
customMenu = new QMenu(&view);
customMenu->addAction("Action1");
customMenu->addAction("Action2");
customMenu->popup(pt);
});
}
view.setContextMenuPolicy(contextMenuPolicy);
// input is supposed to be skipped before first real navigation in >= 79
QSignalSpy loadSpy(&view, &QWebEngineView::loadFinished);
view.load(QUrl("about:blank"));
view.resize(640, 480);
view.show();
QTRY_COMPARE(loadSpy.count(), 1);
QVERIFY(view.findChildren<QMenu *>().isEmpty());
QTest::mouseMove(view.windowHandle(), QPoint(10,10));
QTest::mouseClick(view.windowHandle(), Qt::RightButton);
// verify for zero children will always succeed, so should be tested with at least minor timeout
if (childrenCount <= 0) {
QVERIFY(!QTest::qWaitFor([&view] () { return view.findChildren<QMenu *>().count() > 0; }, 500));
} else {
QTRY_COMPARE(view.findChildren<QMenu *>().count(), childrenCount);
if (isCustomMenu) {
QCOMPARE(view.findChildren<QMenu *>().first(), customMenu);
}
}
QCOMPARE(!!customMenu, isCustomMenu);
}
void tst_QWebEngineView::mouseLeave()
{
QScopedPointer<QWidget> containerWidget(new QWidget);
QLabel *label = new QLabel(containerWidget.data());
label->setStyleSheet("background-color: red;");
label->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed));
label->setMinimumHeight(100);
QWebEngineView *view = new QWebEngineView(containerWidget.data());
view->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed));
view->setMinimumHeight(100);
QVBoxLayout *layout = new QVBoxLayout;
layout->setAlignment(Qt::AlignTop);
layout->setSpacing(0);
layout->setContentsMargins(0, 0, 0, 0);
layout->addWidget(label);
layout->addWidget(view);
containerWidget->setLayout(layout);
containerWidget->show();
QVERIFY(QTest::qWaitForWindowExposed(containerWidget.data()));
QTest::mouseMove(containerWidget->windowHandle(), QPoint(1, 1));
auto innerText = [view]() -> QString {
return evaluateJavaScriptSync(view->page(), "document.getElementById('testDiv').innerText").toString();
};
QSignalSpy loadFinishedSpy(view, SIGNAL(loadFinished(bool)));
view->setHtml("<html>"
"<head><script>"
"function init() {"
" var div = document.getElementById('testDiv');"
" div.onmouseenter = function(e) { div.innerText = 'Mouse IN' };"
" div.onmouseleave = function(e) { div.innerText = 'Mouse OUT' };"
"}"
"</script></head>"
"<body onload='init()' style='margin: 0px; padding: 0px'>"
" <div id='testDiv' style='width: 100%; height: 100%; background-color: green' />"
"</body>"
"</html>");
QVERIFY(loadFinishedSpy.wait());
// Make sure the testDiv text is empty.
evaluateJavaScriptSync(view->page(), "document.getElementById('testDiv').innerText = ''");
QTRY_VERIFY(innerText().isEmpty());
QTest::mouseMove(containerWidget->windowHandle(), QPoint(50, 150));
QTRY_COMPARE(innerText(), QStringLiteral("Mouse IN"));
QTest::mouseMove(containerWidget->windowHandle(), QPoint(50, 50));
QTRY_COMPARE(innerText(), QStringLiteral("Mouse OUT"));
}
void tst_QWebEngineView::webUIURLs_data()
{
QTest::addColumn<QUrl>("url");
QTest::addColumn<bool>("supported");
QTest::newRow("about") << QUrl("chrome://about") << false;
QTest::newRow("accessibility") << QUrl("chrome://accessibility") << true;
QTest::newRow("appcache-internals") << QUrl("chrome://appcache-internals") << true;
QTest::newRow("apps") << QUrl("chrome://apps") << false;
QTest::newRow("blob-internals") << QUrl("chrome://blob-internals") << true;
QTest::newRow("bluetooth-internals") << QUrl("chrome://bluetooth-internals") << false;
QTest::newRow("bookmarks") << QUrl("chrome://bookmarks") << false;
QTest::newRow("cache") << QUrl("chrome://cache") << false;
QTest::newRow("chrome") << QUrl("chrome://chrome") << false;
QTest::newRow("chrome-urls") << QUrl("chrome://chrome-urls") << false;
QTest::newRow("components") << QUrl("chrome://components") << false;
QTest::newRow("crashes") << QUrl("chrome://crashes") << false;
QTest::newRow("credits") << QUrl("chrome://credits") << false;
QTest::newRow("device-log") << QUrl("chrome://device-log") << false;
QTest::newRow("devices") << QUrl("chrome://devices") << false;
QTest::newRow("dino") << QUrl("chrome://dino") << false; // It works but this is an error page
QTest::newRow("dns") << QUrl("chrome://dns") << false;
QTest::newRow("downloads") << QUrl("chrome://downloads") << false;
QTest::newRow("extensions") << QUrl("chrome://extensions") << false;
QTest::newRow("flags") << QUrl("chrome://flags") << false;
QTest::newRow("flash") << QUrl("chrome://flash") << false;
QTest::newRow("gcm-internals") << QUrl("chrome://gcm-internals") << false;
QTest::newRow("gpu") << QUrl("chrome://gpu") << true;
QTest::newRow("help") << QUrl("chrome://help") << false;
QTest::newRow("histograms") << QUrl("chrome://histograms") << true;
QTest::newRow("indexeddb-internals") << QUrl("chrome://indexeddb-internals") << true;
QTest::newRow("inspect") << QUrl("chrome://inspect") << false;
QTest::newRow("invalidations") << QUrl("chrome://invalidations") << false;
QTest::newRow("linux-proxy-config") << QUrl("chrome://linux-proxy-config") << false;
QTest::newRow("local-state") << QUrl("chrome://local-state") << false;
QTest::newRow("media-internals") << QUrl("chrome://media-internals") << true;
QTest::newRow("net-export") << QUrl("chrome://net-export") << false;
QTest::newRow("net-internals") << QUrl("chrome://net-internals") << false;
QTest::newRow("network-error") << QUrl("chrome://network-error") << false;
QTest::newRow("network-errors") << QUrl("chrome://network-errors") << true;
QTest::newRow("newtab") << QUrl("chrome://newtab") << false;
QTest::newRow("ntp-tiles-internals") << QUrl("chrome://ntp-tiles-internals") << false;
QTest::newRow("omnibox") << QUrl("chrome://omnibox") << false;
QTest::newRow("password-manager-internals") << QUrl("chrome://password-manager-internals") << false;
QTest::newRow("policy") << QUrl("chrome://policy") << false;
QTest::newRow("predictors") << QUrl("chrome://predictors") << false;
QTest::newRow("print") << QUrl("chrome://print") << false;
QTest::newRow("process-internals") << QUrl("chrome://process-internals") << true;
QTest::newRow("profiler") << QUrl("chrome://profiler") << false;
QTest::newRow("quota-internals") << QUrl("chrome://quota-internals") << true;
QTest::newRow("safe-browsing") << QUrl("chrome://safe-browsing") << false;
#ifdef Q_OS_LINUX
QTest::newRow("sandbox") << QUrl("chrome://sandbox") << true;
#else
QTest::newRow("sandbox") << QUrl("chrome://sandbox") << false;
#endif
QTest::newRow("serviceworker-internals") << QUrl("chrome://serviceworker-internals") << true;
QTest::newRow("settings") << QUrl("chrome://settings") << false;
QTest::newRow("signin-internals") << QUrl("chrome://signin-internals") << false;
QTest::newRow("site-engagement") << QUrl("chrome://site-engagement") << false;
QTest::newRow("suggestions") << QUrl("chrome://suggestions") << false;
QTest::newRow("supervised-user-internals") << QUrl("chrome://supervised-user-internals") << false;
QTest::newRow("sync-internals") << QUrl("chrome://sync-internals") << false;
QTest::newRow("system") << QUrl("chrome://system") << false;
QTest::newRow("terms") << QUrl("chrome://terms") << false;
QTest::newRow("thumbnails") << QUrl("chrome://thumbnails") << false;
QTest::newRow("tracing") << QUrl("chrome://tracing") << false;
QTest::newRow("translate-internals") << QUrl("chrome://translate-internals") << false;
QTest::newRow("usb-internals") << QUrl("chrome://usb-internals") << false;
QTest::newRow("user-actions") << QUrl("chrome://user-actions") << false;
QTest::newRow("version") << QUrl("chrome://version") << false;
QTest::newRow("webrtc-internals") << QUrl("chrome://webrtc-internals") << true;
QTest::newRow("webrtc-logs") << QUrl("chrome://webrtc-logs") << false;
}
void tst_QWebEngineView::webUIURLs()
{
QFETCH(QUrl, url);
QFETCH(bool, supported);
QWebEngineView view;
view.settings()->setAttribute(QWebEngineSettings::ErrorPageEnabled, false);
QSignalSpy loadFinishedSpy(&view, SIGNAL(loadFinished(bool)));
view.load(url);
QTRY_COMPARE_WITH_TIMEOUT(loadFinishedSpy.count(), 1, 30000);
QCOMPARE(loadFinishedSpy.takeFirst().at(0).toBool(), supported);
}
void tst_QWebEngineView::visibilityState()
{
QWebEngineView view;
QSignalSpy spy(&view, &QWebEngineView::loadFinished);
view.load(QStringLiteral("about:blank"));
QVERIFY(spy.count() || spy.wait());
QVERIFY(spy.takeFirst().takeFirst().toBool());
QCOMPARE(evaluateJavaScriptSync(view.page(), "document.visibilityState").toString(), QStringLiteral("hidden"));
view.show();
QVERIFY(QTest::qWaitForWindowExposed(&view));
QCOMPARE(evaluateJavaScriptSync(view.page(), "document.visibilityState").toString(), QStringLiteral("visible"));
}
void tst_QWebEngineView::visibilityState2()
{
QWebEngineView view;
QSignalSpy spy(&view, &QWebEngineView::loadFinished);
view.show();
view.load(QStringLiteral("about:blank"));
view.hide();
QVERIFY(spy.count() || spy.wait());
QVERIFY(spy.takeFirst().takeFirst().toBool());
QCOMPARE(evaluateJavaScriptSync(view.page(), "document.visibilityState").toString(), QStringLiteral("hidden"));
}
void tst_QWebEngineView::visibilityState3()
{
QWebEnginePage page1;
QWebEnginePage page2;
QSignalSpy spy1(&page1, &QWebEnginePage::loadFinished);
QSignalSpy spy2(&page2, &QWebEnginePage::loadFinished);
page1.load(QStringLiteral("about:blank"));
page2.load(QStringLiteral("about:blank"));
QVERIFY(spy1.count() || spy1.wait());
QVERIFY(spy2.count() || spy2.wait());
QWebEngineView view;
view.setPage(&page1);
view.show();
QCOMPARE(evaluateJavaScriptSync(&page1, "document.visibilityState").toString(), QStringLiteral("visible"));
QCOMPARE(evaluateJavaScriptSync(&page2, "document.visibilityState").toString(), QStringLiteral("hidden"));
view.setPage(&page2);
QCOMPARE(evaluateJavaScriptSync(&page1, "document.visibilityState").toString(), QStringLiteral("hidden"));
QCOMPARE(evaluateJavaScriptSync(&page2, "document.visibilityState").toString(), QStringLiteral("visible"));
}
void tst_QWebEngineView::jsKeyboardEvent_data()
{
QTest::addColumn<char>("key");
QTest::addColumn<Qt::KeyboardModifiers>("modifiers");
QTest::addColumn<QString>("expected");
#if defined(Q_OS_MACOS)
// See Qt::AA_MacDontSwapCtrlAndMeta
Qt::KeyboardModifiers controlModifier = Qt::MetaModifier;
#else
Qt::KeyboardModifiers controlModifier = Qt::ControlModifier;
#endif
QTest::newRow("Ctrl+Shift+A") << 'A' << (controlModifier | Qt::ShiftModifier) << QStringLiteral(
"16,ShiftLeft,Shift,false,true,false;"
"17,ControlLeft,Control,true,true,false;"
"65,KeyA,A,true,true,false;");
QTest::newRow("Ctrl+z") << 'z' << controlModifier << QStringLiteral(
"17,ControlLeft,Control,true,false,false;"
"90,KeyZ,z,true,false,false;");
}
void tst_QWebEngineView::jsKeyboardEvent()
{
QWebEngineView view;
evaluateJavaScriptSync(
view.page(),
"var log = '';"
"addEventListener('keydown', (ev) => {"
" log += [ev.keyCode, ev.code, ev.key, ev.ctrlKey, ev.shiftKey, ev.altKey].join(',') + ';';"
"});");
QFETCH(char, key);
QFETCH(Qt::KeyboardModifiers, modifiers);
QFETCH(QString, expected);
// Note that this only tests the fallback code path where native scan codes are not used.
QTest::keyClick(view.focusProxy(), key, modifiers);
QTRY_VERIFY(evaluateJavaScriptSync(view.page(), "log") != QVariant(QString()));
QCOMPARE(evaluateJavaScriptSync(view.page(), "log"), expected);
}
void tst_QWebEngineView::deletePage()
{
QWebEngineView view;
QWebEnginePage *page = view.page();
QVERIFY(page);
QCOMPARE(page->parent(), &view);
delete page;
// Test that a new page is created and that it is useful:
QVERIFY(view.page());
QSignalSpy spy(view.page(), &QWebEnginePage::loadFinished);
view.page()->load(QStringLiteral("about:blank"));
QTRY_VERIFY(spy.count());
}
class TestView : public QWebEngineView {
Q_OBJECT
public:
TestView(QWidget *parent = nullptr) : QWebEngineView(parent)
{
}
QWebEngineView *createWindow(QWebEnginePage::WebWindowType) override
{
TestView *view = new TestView(parentWidget());
createdWindows.append(view);
return view;
}
QList<TestView *> createdWindows;
};
void tst_QWebEngineView::closeOpenerTab()
{
QWidget rootWidget;
rootWidget.resize(600, 400);
auto *testView = new TestView(&rootWidget);
testView->settings()->setAttribute(QWebEngineSettings::JavascriptCanOpenWindows, true);
QSignalSpy loadFinishedSpy(testView, SIGNAL(loadFinished(bool)));
testView->setUrl(QStringLiteral("about:blank"));
QTRY_VERIFY(loadFinishedSpy.count());
testView->page()->runJavaScript(QStringLiteral("window.open('about:blank','_blank')"));
QTRY_COMPARE(testView->createdWindows.size(), 1);
auto *newView = testView->createdWindows.at(0);
newView->show();
rootWidget.show();
QVERIFY(QTest::qWaitForWindowExposed(newView));
QVERIFY(newView->focusProxy()->isVisible());
delete testView;
QVERIFY(newView->focusProxy()->isVisible());
}
void tst_QWebEngineView::switchPage()
{
QWebEngineProfile profile;
QWebEnginePage page1(&profile);
QWebEnginePage page2(&profile);
QSignalSpy loadFinishedSpy1(&page1, SIGNAL(loadFinished(bool)));
QSignalSpy loadFinishedSpy2(&page2, SIGNAL(loadFinished(bool)));
page1.setHtml("<html><body bgcolor=\"#000000\"></body></html>");
page2.setHtml("<html><body bgcolor=\"#ffffff\"></body></html>");
QTRY_VERIFY(loadFinishedSpy1.count() && loadFinishedSpy2.count());
QWebEngineView webView;
webView.resize(300,300);
webView.show();
webView.setPage(&page1);
QTRY_COMPARE(webView.grab().toImage().pixelColor(QPoint(150,150)), Qt::black);
webView.setPage(&page2);
QTRY_COMPARE(webView.grab().toImage().pixelColor(QPoint(150,150)), Qt::white);
webView.setPage(&page1);
QTRY_COMPARE(webView.grab().toImage().pixelColor(QPoint(150,150)), Qt::black);
}
void tst_QWebEngineView::setPageDeletesImplicitPage()
{
QWebEngineView view;
QPointer<QWebEnginePage> implicitPage = view.page();
QWebEnginePage explicitPage;
view.setPage(&explicitPage);
QCOMPARE(view.page(), &explicitPage);
QVERIFY(!implicitPage); // should be deleted
}
void tst_QWebEngineView::setPageDeletesImplicitPage2()
{
QWebEngineView view1;
QWebEngineView view2;
QPointer<QWebEnginePage> implicitPage = view1.page();
view2.setPage(view1.page());
QVERIFY(implicitPage);
QVERIFY(view1.page() != implicitPage);
QWebEnginePage explicitPage;
view2.setPage(&explicitPage);
QCOMPARE(view2.page(), &explicitPage);
QVERIFY(!implicitPage); // should be deleted
}
void tst_QWebEngineView::setViewDeletesImplicitPage()
{
QWebEngineView view;
QPointer<QWebEnginePage> implicitPage = view.page();
QWebEnginePage explicitPage;
explicitPage.setView(&view);
QCOMPARE(view.page(), &explicitPage);
QVERIFY(!implicitPage); // should be deleted
}
void tst_QWebEngineView::setPagePreservesExplicitPage()
{
QWebEngineView view;
QPointer<QWebEnginePage> explicitPage1 = new QWebEnginePage(&view);
QPointer<QWebEnginePage> explicitPage2 = new QWebEnginePage(&view);
view.setPage(explicitPage1.data());
view.setPage(explicitPage2.data());
QCOMPARE(view.page(), explicitPage2.data());
QVERIFY(explicitPage1); // should not be deleted
}
void tst_QWebEngineView::setViewPreservesExplicitPage()
{
QWebEngineView view;
QPointer<QWebEnginePage> explicitPage1 = new QWebEnginePage(&view);
QPointer<QWebEnginePage> explicitPage2 = new QWebEnginePage(&view);
explicitPage1->setView(&view);
explicitPage2->setView(&view);
QCOMPARE(view.page(), explicitPage2.data());
QVERIFY(explicitPage1); // should not be deleted
}
void tst_QWebEngineView::closeDiscardsPage()
{
QWebEngineProfile profile;
QWebEnginePage page(&profile);
QWebEngineView view;
view.setPage(&page);
view.resize(300, 300);
view.show();
QVERIFY(QTest::qWaitForWindowExposed(&view));
QCOMPARE(page.isVisible(), true);
QCOMPARE(page.lifecycleState(), QWebEnginePage::LifecycleState::Active);
view.close();
QCOMPARE(page.isVisible(), false);
QCOMPARE(page.lifecycleState(), QWebEnginePage::LifecycleState::Discarded);
}
QTEST_MAIN(tst_QWebEngineView)
#include "tst_qwebengineview.moc"