blob: a3c45918bac88badc80bfccbfb59264927dae6c1 [file] [log] [blame]
/*
Copyright (C) 2016 The Qt Company Ltd.
Copyright (C) 2009 Girish Ramakrishnan <girish@forwardbias.in>
Copyright (C) 2010 Holger Hans Peter Freyther
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 "../util.h"
#include <QtWebEngineCore/qtwebenginecore-config.h>
#include <QByteArray>
#include <QClipboard>
#include <QDir>
#include <QGraphicsWidget>
#include <QHBoxLayout>
#include <QLineEdit>
#include <QMainWindow>
#include <QMenu>
#include <QMimeDatabase>
#include <QNetworkProxy>
#include <QOpenGLWidget>
#include <QPaintEngine>
#include <QPushButton>
#include <QScreen>
#include <QStateMachine>
#include <QtGui/QClipboard>
#include <QtTest/QtTest>
#include <QTextCharFormat>
#if QT_CONFIG(webengine_webchannel)
#include <QWebChannel>
#endif
#include <httpserver.h>
#include <qnetworkcookiejar.h>
#include <qnetworkreply.h>
#include <qnetworkrequest.h>
#include <qwebenginedownloaditem.h>
#include <qwebenginefindtextresult.h>
#include <qwebenginefullscreenrequest.h>
#include <qwebenginehistory.h>
#include <qwebenginenotification.h>
#include <qwebenginepage.h>
#include <qwebengineprofile.h>
#include <qwebenginequotarequest.h>
#include <qwebengineregisterprotocolhandlerrequest.h>
#include <qwebenginescript.h>
#include <qwebenginescriptcollection.h>
#include <qwebenginesettings.h>
#include <qwebengineview.h>
#include <qimagewriter.h>
static void removeRecursive(const QString& dirname)
{
QDir dir(dirname);
QFileInfoList entries(dir.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot));
for (int i = 0; i < entries.count(); ++i)
if (entries[i].isDir())
removeRecursive(entries[i].filePath());
else
dir.remove(entries[i].fileName());
QDir().rmdir(dirname);
}
class tst_QWebEnginePage : public QObject
{
Q_OBJECT
public:
tst_QWebEnginePage();
virtual ~tst_QWebEnginePage();
public Q_SLOTS:
void init();
void cleanup();
void cleanupFiles();
private Q_SLOTS:
void initTestCase();
void cleanupTestCase();
void comboBoxPopupPositionAfterMove();
void comboBoxPopupPositionAfterChildMove();
void acceptNavigationRequest();
void acceptNavigationRequestNavigationType();
void acceptNavigationRequestRelativeToNothing();
void geolocationRequestJS_data();
void geolocationRequestJS();
void loadFinished();
void actionStates();
void pasteImage();
void popupFormSubmission();
void callbackSpyDeleted();
void multipleProfilesAndLocalStorage();
void textSelection();
void backActionUpdate();
void localStorageVisibility();
void consoleOutput();
void userAgentNewlineStripping();
void renderWidgetHostViewNotShowTopLevel();
void getUserMediaRequest_data();
void getUserMediaRequest();
void getUserMediaRequestDesktopAudio();
void getUserMediaRequestSettingDisabled();
void getUserMediaRequestDesktopVideoManyPages();
void getUserMediaRequestDesktopVideoManyRequests();
void savePage();
void crashTests_LazyInitializationOfMainFrame();
#if defined(ENABLE_WEBGL) && ENABLE_WEBGL
void acceleratedWebGLScreenshotWithoutView();
void unacceleratedWebGLScreenshotWithoutView();
#endif
void testJSPrompt();
void findText();
void findTextResult();
void findTextSuccessiveShouldCallAllCallbacks();
void findTextCalledOnMatch();
void findTextActiveMatchOrdinal();
void deleteQWebEngineViewTwice();
void loadSignalsOrder_data();
void loadSignalsOrder();
void openWindowDefaultSize();
#ifdef Q_OS_MAC
void macCopyUnicodeToClipboard();
#endif
void runJavaScript();
void runJavaScriptDisabled();
void runJavaScriptFromSlot();
void fullScreenRequested();
void quotaRequested();
// Tests from tst_QWebEngineFrame
void symmetricUrl();
void progressSignal();
void urlChange();
void requestedUrlAfterSetAndLoadFailures();
void asyncAndDelete();
void earlyToHtml();
void setHtml();
void setHtmlWithImageResource();
void setHtmlWithStylesheetResource();
void setHtmlWithBaseURL();
void setHtmlWithJSAlert();
void setHtmlWithModuleImport();
void baseUrl_data();
void baseUrl();
void scrollPosition();
void scrollbarsOff();
void evaluateWillCauseRepaint();
void setContent_data();
void setContent();
void setUrlWithPendingLoads();
void setUrlToEmpty();
void setUrlToInvalid();
void setUrlToBadDomain();
void setUrlToBadPort();
void setUrlHistory();
void setUrlUsingStateObject();
void setUrlThenLoads_data();
void setUrlThenLoads();
void loadFinishedAfterNotFoundError();
void loadInSignalHandlers_data();
void loadInSignalHandlers();
void loadFromQrc();
#if QT_CONFIG(webengine_webchannel)
void restoreHistory();
#endif
void toPlainTextLoadFinishedRace_data();
void toPlainTextLoadFinishedRace();
void setZoomFactor();
void mouseButtonTranslation();
void mouseMovementProperties();
void viewSource();
void viewSourceURL_data();
void viewSourceURL();
void viewSourceCredentials();
void proxyConfigWithUnexpectedHostPortPair();
void registerProtocolHandler_data();
void registerProtocolHandler();
void dataURLFragment();
void devTools();
void openLinkInDifferentProfile();
void openLinkInNewPage_data();
void openLinkInNewPage();
void triggerActionWithoutMenu();
void dynamicFrame();
void notificationPermission_data();
void notificationPermission();
void sendNotification();
void contentsSize();
void setLifecycleState();
void setVisible();
void discardPreservesProperties();
void discardBeforeInitialization();
void automaticUndiscard();
void setLifecycleStateWithDevTools();
void discardPreservesCommittedLoad();
void discardAbortsPendingLoad();
void discardAbortsPendingLoadAndPreservesCommittedLoad();
void recommendedState();
void recommendedStateAuto();
void setLifecycleStateAndReload();
void editActionsWithExplicitFocus();
void editActionsWithInitialFocus();
void editActionsWithFocusOnIframe();
void editActionsWithoutSelection();
void customUserAgentInNewTab();
void renderProcessCrashed();
void renderProcessPid();
void backgroundColor();
void audioMuted();
void closeContents();
void isSafeRedirect_data();
void isSafeRedirect();
private:
static QPoint elementCenter(QWebEnginePage *page, const QString &id);
static bool isFalseJavaScriptResult(QWebEnginePage *page, const QString &javaScript);
static bool isTrueJavaScriptResult(QWebEnginePage *page, const QString &javaScript);
static bool isEmptyListJavaScriptResult(QWebEnginePage *page, const QString &javaScript);
QWebEngineView* m_view;
QWebEnginePage* m_page;
QString tmpDirPath() const
{
static QString tmpd = QDir::tempPath() + "/tst_qwebenginepage-"
+ QDateTime::currentDateTime().toString(QLatin1String("yyyyMMddhhmmss"));
return tmpd;
}
};
tst_QWebEnginePage::tst_QWebEnginePage()
{
}
tst_QWebEnginePage::~tst_QWebEnginePage()
{
}
void tst_QWebEnginePage::init()
{
m_view = new QWebEngineView();
m_page = m_view->page();
m_page->settings()->setAttribute(QWebEngineSettings::ErrorPageEnabled, false);
m_view->settings()->setAttribute(QWebEngineSettings::FocusOnNavigationEnabled, true);
}
void tst_QWebEnginePage::cleanup()
{
delete m_view;
}
void tst_QWebEnginePage::cleanupFiles()
{
removeRecursive(tmpDirPath());
}
void tst_QWebEnginePage::initTestCase()
{
QLocale::setDefault(QLocale("en"));
cleanupFiles(); // In case there are old files from previous runs
// Set custom path since the CI doesn't install test plugins.
// Stolen from qtlocation/tests/auto/positionplugintest.
QString searchPath = QCoreApplication::applicationDirPath();
#ifdef Q_OS_WIN
searchPath += QStringLiteral("/..");
#endif
searchPath += QStringLiteral("/../../../plugins");
QCoreApplication::addLibraryPath(searchPath);
}
void tst_QWebEnginePage::cleanupTestCase()
{
cleanupFiles(); // Be nice
}
class NavigationRequestOverride : public QWebEnginePage
{
public:
NavigationRequestOverride(QWebEngineProfile* profile, bool initialValue) : QWebEnginePage(profile, nullptr), m_acceptNavigationRequest(initialValue) {}
bool m_acceptNavigationRequest;
protected:
virtual bool acceptNavigationRequest(const QUrl &url, NavigationType type, bool isMainFrame)
{
Q_UNUSED(url);
Q_UNUSED(isMainFrame);
if (type == QWebEnginePage::NavigationTypeTyped)
return true;
return m_acceptNavigationRequest;
}
};
void tst_QWebEnginePage::acceptNavigationRequest()
{
QWebEngineProfile profile;
NavigationRequestOverride page(&profile, false);
QSignalSpy loadSpy(&page, SIGNAL(loadFinished(bool)));
page.setHtml(QString("<html><body><form name='tstform' action='data:text/html,foo'method='get'>"
"<input type='text'><input type='submit'></form></body></html>"), QUrl());
QTRY_COMPARE_WITH_TIMEOUT(loadSpy.count(), 1, 20000);
evaluateJavaScriptSync(&page, "tstform.submit();");
QTRY_COMPARE(loadSpy.count(), 2);
// Content hasn't changed so the form submit will still work
page.m_acceptNavigationRequest = true;
evaluateJavaScriptSync(&page, "tstform.submit();");
QTRY_COMPARE(loadSpy.count(), 3);
// Now the content has changed
QCOMPARE(toPlainTextSync(&page), QString("foo?"));
}
class JSTestPage : public QWebEnginePage
{
Q_OBJECT
public:
JSTestPage(QObject* parent = 0)
: QWebEnginePage(parent) {}
virtual bool shouldInterruptJavaScript()
{
return true;
}
public Q_SLOTS:
void requestPermission(const QUrl &origin, QWebEnginePage::Feature feature)
{
if (m_allowGeolocation)
setFeaturePermission(origin, feature, PermissionGrantedByUser);
else
setFeaturePermission(origin, feature, PermissionDeniedByUser);
}
public:
void setGeolocationPermission(bool allow)
{
m_allowGeolocation = allow;
}
private:
bool m_allowGeolocation;
};
void tst_QWebEnginePage::geolocationRequestJS_data()
{
QTest::addColumn<bool>("allowed");
QTest::addColumn<int>("errorCode");
QTest::newRow("allowed") << true << 0;
QTest::newRow("not allowed") << false << 1;
}
void tst_QWebEnginePage::geolocationRequestJS()
{
QFETCH(bool, allowed);
QFETCH(int, errorCode);
QWebEngineView view;
JSTestPage *newPage = new JSTestPage(&view);
newPage->setView(&view);
newPage->setGeolocationPermission(allowed);
connect(newPage, SIGNAL(featurePermissionRequested(const QUrl&, QWebEnginePage::Feature)),
newPage, SLOT(requestPermission(const QUrl&, QWebEnginePage::Feature)));
QSignalSpy spyLoadFinished(newPage, SIGNAL(loadFinished(bool)));
newPage->setHtml(QString("<html><body>test</body></html>"), QUrl("qrc://secure/origin"));
QTRY_COMPARE_WITH_TIMEOUT(spyLoadFinished.count(), 1, 20000);
// Geolocation is only enabled for visible WebContents.
view.show();
QVERIFY(QTest::qWaitForWindowExposed(&view));
if (evaluateJavaScriptSync(newPage, QLatin1String("!navigator.geolocation")).toBool())
W_QSKIP("Geolocation is not supported.", SkipSingle);
evaluateJavaScriptSync(newPage, "var errorCode = 0; var done = false; function error(err) { errorCode = err.code; done = true; } function success(pos) { done = true; } navigator.geolocation.getCurrentPosition(success, error)");
QTRY_VERIFY(evaluateJavaScriptSync(newPage, "done").toBool());
int result = evaluateJavaScriptSync(newPage, "errorCode").toInt();
if (result == 2)
QEXPECT_FAIL("", "No location service available.", Continue);
QCOMPARE(result, errorCode);
}
void tst_QWebEnginePage::loadFinished()
{
QWebEnginePage page;
QSignalSpy spyLoadStarted(&page, SIGNAL(loadStarted()));
QSignalSpy spyLoadFinished(&page, SIGNAL(loadFinished(bool)));
page.load(QUrl("data:text/html,<frameset cols=\"25%,75%\"><frame src=\"data:text/html,"
"<head><meta http-equiv='refresh' content='1'></head>foo \">"
"<frame src=\"data:text/html,bar\"></frameset>"));
QTRY_COMPARE_WITH_TIMEOUT(spyLoadFinished.count(), 1, 20000);
QEXPECT_FAIL("", "Behavior change: Load signals are emitted only for the main frame in QtWebEngine.", Continue);
QTRY_VERIFY_WITH_TIMEOUT(spyLoadStarted.count() > 1, 100);
QEXPECT_FAIL("", "Behavior change: Load signals are emitted only for the main frame in QtWebEngine.", Continue);
QTRY_VERIFY_WITH_TIMEOUT(spyLoadFinished.count() > 1, 100);
spyLoadFinished.clear();
page.load(QUrl("data:text/html,<frameset cols=\"25%,75%\"><frame src=\"data:text/html,"
"foo \"><frame src=\"data:text/html,bar\"></frameset>"));
QTRY_COMPARE(spyLoadFinished.count(), 1);
QCOMPARE(spyLoadFinished.count(), 1);
}
void tst_QWebEnginePage::actionStates()
{
m_page->load(QUrl("qrc:///resources/script.html"));
QAction* reloadAction = m_page->action(QWebEnginePage::Reload);
QAction* stopAction = m_page->action(QWebEnginePage::Stop);
QTRY_VERIFY(reloadAction->isEnabled());
QTRY_VERIFY(!stopAction->isEnabled());
}
static QImage imageWithoutAlpha(const QImage &image)
{
QImage result = image;
QPainter painter(&result);
painter.fillRect(result.rect(), Qt::green);
painter.drawImage(0, 0, image);
return result;
}
void tst_QWebEnginePage::callbackSpyDeleted()
{
QWebEnginePage *page = m_view->page();
CallbackSpy<QVariant> spy;
QString poorManSleep("function wait(ms){"
" var start = new Date().getTime();"
" var end = start;"
" while (start + ms > end) {"
"end = new Date().getTime();"
" }"
"}"
"wait(1000);");
page->runJavaScript(poorManSleep, spy.ref());
//spy deleted before callback
}
void tst_QWebEnginePage::pasteImage()
{
// Pixels with an alpha value of 0 will have different RGB values after the
// test -> clipboard -> webengine -> test roundtrip.
// Clear the alpha channel to make QCOMPARE happy.
const QImage origImage = imageWithoutAlpha(QImage(":/resources/image.png"));
QClipboard *clipboard = QGuiApplication::clipboard();
clipboard->setImage(origImage);
QWebEnginePage *page = m_view->page();
QSignalSpy spyFinished(m_view, &QWebEngineView::loadFinished);
page->load(QUrl("qrc:///resources/pasteimage.html"));
QTRY_VERIFY_WITH_TIMEOUT(!spyFinished.isEmpty(), 20000);
page->triggerAction(QWebEnginePage::Paste);
QTRY_VERIFY(evaluateJavaScriptSync(page,
"window.myImageDataURL ? window.myImageDataURL.length : 0").toInt() > 0);
QByteArray data = evaluateJavaScriptSync(page, "window.myImageDataURL").toByteArray();
data.remove(0, data.indexOf(";base64,") + 8);
QImage image = QImage::fromData(QByteArray::fromBase64(data), "PNG");
if (image.format() == QImage::Format_RGB32)
image.reinterpretAsFormat(QImage::Format_ARGB32);
QCOMPARE(image, origImage);
}
class ConsolePage : public QWebEnginePage
{
public:
ConsolePage(QObject* parent = 0) : QWebEnginePage(parent) {}
virtual void javaScriptConsoleMessage(JavaScriptConsoleMessageLevel level, const QString& message, int lineNumber, const QString& sourceID)
{
levels.append(level);
messages.append(message);
lineNumbers.append(lineNumber);
sourceIDs.append(sourceID);
}
QList<int> levels;
QStringList messages;
QList<int> lineNumbers;
QStringList sourceIDs;
};
void tst_QWebEnginePage::consoleOutput()
{
ConsolePage page;
// We don't care about the result but want this to be synchronous
evaluateJavaScriptSync(&page, "this is not valid JavaScript");
QCOMPARE(page.messages.count(), 1);
QCOMPARE(page.lineNumbers.at(0), 1);
}
class TestPage : public QWebEnginePage {
Q_OBJECT
public:
TestPage(QObject* parent = 0) : QWebEnginePage(parent)
{
connect(this, SIGNAL(geometryChangeRequested(QRect)), this, SLOT(slotGeometryChangeRequested(QRect)));
}
struct Navigation {
NavigationType type;
QUrl url;
bool isMainFrame;
};
QList<Navigation> navigations;
virtual bool acceptNavigationRequest(const QUrl &url, NavigationType type, bool isMainFrame)
{
Navigation n;
n.url = url;
n.type = type;
n.isMainFrame = isMainFrame;
navigations.append(n);
return true;
}
QList<TestPage*> createdWindows;
virtual QWebEnginePage* createWindow(WebWindowType) {
TestPage* page = new TestPage(this);
createdWindows.append(page);
emit windowCreated();
return page;
}
QRect requestedGeometry;
signals:
void windowCreated();
private Q_SLOTS:
void slotGeometryChangeRequested(const QRect& geom) {
requestedGeometry = geom;
}
};
void tst_QWebEnginePage::acceptNavigationRequestNavigationType()
{
TestPage page;
QSignalSpy loadSpy(&page, SIGNAL(loadFinished(bool)));
page.load(QUrl("qrc:///resources/script.html"));
QTRY_COMPARE_WITH_TIMEOUT(loadSpy.count(), 1, 20000);
QTRY_COMPARE(page.navigations.count(), 1);
page.load(QUrl("qrc:///resources/content.html"));
QTRY_COMPARE_WITH_TIMEOUT(loadSpy.count(), 2, 20000);
QTRY_COMPARE(page.navigations.count(), 2);
page.triggerAction(QWebEnginePage::Stop);
QVERIFY(page.history()->canGoBack());
page.triggerAction(QWebEnginePage::Back);
QTRY_COMPARE(loadSpy.count(), 3);
QTRY_COMPARE(page.navigations.count(), 3);
page.triggerAction(QWebEnginePage::Reload);
QTRY_COMPARE(loadSpy.count(), 4);
QTRY_COMPARE(page.navigations.count(), 4);
page.load(QUrl("qrc:///resources/reload.html"));
QTRY_COMPARE_WITH_TIMEOUT(loadSpy.count(), 6, 20000);
QTRY_COMPARE(page.navigations.count(), 6);
QList<QWebEnginePage::NavigationType> expectedList;
expectedList << QWebEnginePage::NavigationTypeTyped
<< QWebEnginePage::NavigationTypeTyped
<< QWebEnginePage::NavigationTypeBackForward
<< QWebEnginePage::NavigationTypeReload
<< QWebEnginePage::NavigationTypeTyped
<< QWebEnginePage::NavigationTypeRedirect;
QVERIFY(expectedList.count() == page.navigations.count());
for (int i = 0; i < expectedList.count(); ++i) {
QCOMPARE(page.navigations[i].type, expectedList[i]);
}
}
// Relative url without base url.
//
// See also: QTBUG-48435
void tst_QWebEnginePage::acceptNavigationRequestRelativeToNothing()
{
TestPage page;
QSignalSpy loadSpy(&page, SIGNAL(loadFinished(bool)));
page.setHtml(QString("<html><body><a id='link' href='S0'>limited time offer</a></body></html>"),
/* baseUrl: */ QUrl());
QTRY_COMPARE_WITH_TIMEOUT(loadSpy.count(), 1, 20000);
page.runJavaScript(QStringLiteral("document.getElementById(\"link\").click()"));
QTRY_COMPARE_WITH_TIMEOUT(loadSpy.count(), 2, 20000);
page.setHtml(QString("<html><body><a id='link' href='S0'>limited time offer</a></body></html>"),
/* baseUrl: */ QString("qrc:/"));
QTRY_COMPARE_WITH_TIMEOUT(loadSpy.count(), 3, 20000);
page.runJavaScript(QStringLiteral("document.getElementById(\"link\").click()"));
QTRY_COMPARE_WITH_TIMEOUT(loadSpy.count(), 4, 20000);
// The two setHtml and the second click are counted, while the
// first click is ignored due to the empty base url.
QCOMPARE(page.navigations.count(), 3);
QCOMPARE(page.navigations[0].type, QWebEnginePage::NavigationTypeTyped);
QCOMPARE(page.navigations[1].type, QWebEnginePage::NavigationTypeTyped);
QCOMPARE(page.navigations[2].type, QWebEnginePage::NavigationTypeLinkClicked);
QCOMPARE(page.navigations[2].url, QUrl(QString("qrc:/S0")));
}
void tst_QWebEnginePage::popupFormSubmission()
{
TestPage page;
QSignalSpy loadFinishedSpy(&page, SIGNAL(loadFinished(bool)));
QSignalSpy windowCreatedSpy(&page, SIGNAL(windowCreated()));
page.settings()->setAttribute(QWebEngineSettings::JavascriptCanOpenWindows, true);
page.setHtml("<form name='form1' method=get action='' target='myNewWin'>"
" <input type='hidden' name='foo' value='bar'>"
"</form>");
QTRY_COMPARE_WITH_TIMEOUT(loadFinishedSpy.count(), 1, 20000);
page.runJavaScript("window.open('', 'myNewWin', 'width=500,height=300,toolbar=0');");
evaluateJavaScriptSync(&page, "document.form1.submit();");
QTRY_COMPARE(windowCreatedSpy.count(), 1);
// The number of popup created should be one.
QVERIFY(page.createdWindows.size() == 1);
QTRY_VERIFY(!page.createdWindows[0]->url().isEmpty());
QString url = page.createdWindows[0]->url().toString();
// Check if the form submission was OK.
QVERIFY(url.contains("?foo=bar"));
}
class TestNetworkManager : public QNetworkAccessManager
{
public:
TestNetworkManager(QObject* parent) : QNetworkAccessManager(parent) {}
QList<QUrl> requestedUrls;
QList<QNetworkRequest> requests;
protected:
virtual QNetworkReply* createRequest(Operation op, const QNetworkRequest &request, QIODevice* outgoingData) {
requests.append(request);
requestedUrls.append(request.url());
return QNetworkAccessManager::createRequest(op, request, outgoingData);
}
};
void tst_QWebEnginePage::multipleProfilesAndLocalStorage()
{
QDir dir(tmpDirPath());
bool success = dir.mkpath("path1");
success = success && dir.mkdir("path2");
QVERIFY(success);
{
QWebEngineProfile profile1("test1");
QWebEngineProfile profile2("test2");
profile1.settings()->setAttribute(QWebEngineSettings::LocalStorageEnabled, true);
profile2.settings()->setAttribute(QWebEngineSettings::LocalStorageEnabled, true);
profile1.setPersistentStoragePath(QDir::toNativeSeparators(tmpDirPath() + "/path1"));
profile2.setPersistentStoragePath(QDir::toNativeSeparators(tmpDirPath() + "/path2"));
QWebEnginePage page1(&profile1, nullptr);
QWebEnginePage page2(&profile2, nullptr);
QSignalSpy loadSpy1(&page1, SIGNAL(loadFinished(bool)));
QSignalSpy loadSpy2(&page2, SIGNAL(loadFinished(bool)));
page1.setHtml(QString("<html><body> </body></html>"), QUrl("http://wwww.example.com"));
page2.setHtml(QString("<html><body> </body></html>"), QUrl("http://wwww.example.com"));
QTRY_COMPARE_WITH_TIMEOUT(loadSpy1.count(), 1, 20000);
QTRY_COMPARE_WITH_TIMEOUT(loadSpy2.count(), 1, 20000);
evaluateJavaScriptSync(&page1, "localStorage.setItem('test', 'value1');");
evaluateJavaScriptSync(&page2, "localStorage.setItem('test', 'value2');");
page1.setHtml(QString("<html><body> </body></html>"), QUrl("http://wwww.example.com"));
page2.setHtml(QString("<html><body> </body></html>"), QUrl("http://wwww.example.com"));
QTRY_COMPARE(loadSpy1.count(), 2);
QTRY_COMPARE(loadSpy2.count(), 2);
QVariant s1 = evaluateJavaScriptSync(&page1, "localStorage.getItem('test')");
QCOMPARE(s1.toString(), QString("value1"));
QVariant s2 = evaluateJavaScriptSync(&page2, "localStorage.getItem('test')");
QCOMPARE(s2.toString(), QString("value2"));
}
// Avoid deleting on-disk dbs before the underlying browser-context has been asynchronously deleted
QTest::qWait(1000);
QDir(tmpDirPath() + "/path1").removeRecursively();
QDir(tmpDirPath() + "/path2").removeRecursively();
}
class CursorTrackedPage : public QWebEnginePage
{
public:
CursorTrackedPage(QWidget *parent = 0): QWebEnginePage(parent) {
}
QString jsSelectedText() {
return evaluateJavaScriptSync(this, "window.getSelection().toString()").toString();
}
int selectionStartOffset() {
return evaluateJavaScriptSync(this, "window.getSelection().getRangeAt(0).startOffset").toInt();
}
int selectionEndOffset() {
return evaluateJavaScriptSync(this, "window.getSelection().getRangeAt(0).endOffset").toInt();
}
// true if start offset == end offset, i.e. no selected text
int isSelectionCollapsed() {
return evaluateJavaScriptSync(this, "window.getSelection().getRangeAt(0).collapsed").toBool();
}
};
void tst_QWebEnginePage::textSelection()
{
CursorTrackedPage page;
QString textToSelect("The quick brown fox");
QString content = QString("<html><body><p id=one>%1</p>" \
"<p id=two>jumps over the lazy dog</p>" \
"<p>May the source<br/>be with you!</p></body></html>").arg(textToSelect);
QSignalSpy loadSpy(&page, SIGNAL(loadFinished(bool)));
page.setHtml(content);
QTRY_COMPARE_WITH_TIMEOUT(loadSpy.count(), 1, 20000);
// these actions must exist
QVERIFY(page.action(QWebEnginePage::SelectAll) != 0);
// ..but SelectAll is disabled because the page has no focus due to disabled FocusOnNavigationEnabled.
QCOMPARE(page.action(QWebEnginePage::SelectAll)->isEnabled(), false);
// Verify hasSelection returns false since there is no selection yet...
QVERIFY(!page.hasSelection());
QVERIFY(page.jsSelectedText().isEmpty());
// this will select the first paragraph
QString selectScript = "var range = document.createRange(); " \
"var node = document.getElementById(\"one\"); " \
"range.selectNode(node); " \
"getSelection().addRange(range);";
evaluateJavaScriptSync(&page, selectScript);
// Make sure hasSelection returns true, since there is selected text now...
QTRY_VERIFY(page.hasSelection());
QCOMPARE(page.selectedText().trimmed(), textToSelect);
QCOMPARE(page.jsSelectedText().trimmed(), textToSelect);
// navigate away and check that selection is cleared
page.load(QUrl("about:blank"));
QTRY_COMPARE(loadSpy.count(), 2);
QVERIFY(!page.hasSelection());
QVERIFY(page.selectedText().isEmpty());
QVERIFY(page.jsSelectedText().isEmpty());
}
void tst_QWebEnginePage::backActionUpdate()
{
QWebEngineView view;
view.resize(640, 480);
view.show();
QWebEnginePage *page = view.page();
QSignalSpy loadSpy(page, &QWebEnginePage::loadFinished);
QAction *action = page->action(QWebEnginePage::Back);
QVERIFY(!action->isEnabled());
page->load(QUrl("qrc:///resources/framedindex.html"));
QTRY_COMPARE_WITH_TIMEOUT(loadSpy.count(), 1, 20000);
QVERIFY(!action->isEnabled());
auto firstAnchorCenterInFrame = [](QWebEnginePage *page, const QString &frameName) {
QVariantList rectList = evaluateJavaScriptSync(page,
"(function(){"
"var frame = document.getElementsByName('" + frameName + "')[0];"
"var anchor = frame.contentDocument.getElementsByTagName('a')[0];"
"var rect = anchor.getBoundingClientRect();"
"return [(rect.left + rect.right) / 2, (rect.top + rect.bottom) / 2];"
"})()").toList();
if (rectList.count() != 2) {
qWarning("firstAnchorCenterInFrame failed.");
return QPoint();
}
return QPoint(rectList.at(0).toInt(), rectList.at(1).toInt());
};
QVERIFY(evaluateJavaScriptSync(page, "document.getElementsByName('frame_b')[0].contentDocument == undefined").toBool());
QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, firstAnchorCenterInFrame(page, "frame_c"));
QTRY_VERIFY(evaluateJavaScriptSync(page, "document.getElementsByName('frame_b')[0].contentDocument != undefined").toBool());
QTRY_VERIFY(action->isEnabled());
}
void tst_QWebEnginePage::localStorageVisibility()
{
QWebEngineProfile profile;
QWebEnginePage webPage1(&profile);
QWebEnginePage webPage2(&profile);
webPage1.settings()->setAttribute(QWebEngineSettings::LocalStorageEnabled, true);
webPage2.settings()->setAttribute(QWebEngineSettings::LocalStorageEnabled, false);
QSignalSpy loadSpy1(&webPage1, &QWebEnginePage::loadFinished);
QSignalSpy loadSpy2(&webPage2, &QWebEnginePage::loadFinished);
webPage1.setHtml(QString("<html><body>test</body></html>"), QUrl("http://www.example.com/"));
webPage2.setHtml(QString("<html><body>test</body></html>"), QUrl("http://www.example.com/"));
QTRY_COMPARE_WITH_TIMEOUT(loadSpy1.count(), 1, 20000);
QTRY_COMPARE_WITH_TIMEOUT(loadSpy2.count(), 1, 20000);
// The attribute determines the visibility of the window.localStorage object.
QVERIFY(evaluateJavaScriptSync(&webPage1, QString("(window.localStorage != undefined)")).toBool());
QVERIFY(!evaluateJavaScriptSync(&webPage2, QString("(window.localStorage != undefined)")).toBool());
// Toggle local setting for every page and...
webPage1.settings()->setAttribute(QWebEngineSettings::LocalStorageEnabled, false);
webPage2.settings()->setAttribute(QWebEngineSettings::LocalStorageEnabled, true);
// TODO: note this setting is flaky, consider settings().commit()
// ...first check second page (for storage to appear) as applying settings is batched and done asynchronously
QTRY_VERIFY(evaluateJavaScriptSync(&webPage2, QString("(window.localStorage != undefined)")).toBool());
// Switching the feature off does not actively remove the object from webPage1.
QVERIFY(evaluateJavaScriptSync(&webPage1, QString("(window.localStorage != undefined)")).toBool());
// The object disappears only after reloading.
webPage1.triggerAction(QWebEnginePage::Reload);
webPage2.triggerAction(QWebEnginePage::Reload);
QTRY_COMPARE(loadSpy1.count(), 2);
QTRY_COMPARE(loadSpy2.count(), 2);
QVERIFY(!evaluateJavaScriptSync(&webPage1, QString("(window.localStorage != undefined)")).toBool());
QVERIFY(evaluateJavaScriptSync(&webPage2, QString("(window.localStorage != undefined)")).toBool());
}
void tst_QWebEnginePage::userAgentNewlineStripping()
{
QWebEngineProfile profile;
QWebEnginePage page(&profile);
profile.setHttpUserAgent(QStringLiteral("My User Agent\nX-New-Http-Header: Oh Noes!"));
// The user agent will be updated after a page load.
page.load(QUrl("about:blank"));
QTRY_COMPARE(evaluateJavaScriptSync(&page, "navigator.userAgent").toString(), QStringLiteral("My User Agent X-New-Http-Header: Oh Noes!"));
}
void tst_QWebEnginePage::crashTests_LazyInitializationOfMainFrame()
{
{
QWebEnginePage webPage;
}
{
QWebEnginePage webPage;
webPage.selectedText();
}
{
QWebEnginePage webPage;
webPage.triggerAction(QWebEnginePage::Back, true);
}
}
#if defined(ENABLE_WEBGL) && ENABLE_WEBGL
// https://bugs.webkit.org/show_bug.cgi?id=54138
static void webGLScreenshotWithoutView(bool accelerated)
{
QWebEnginePage page;
page.settings()->setAttribute(QWebEngineSettings::WebGLEnabled, true);
page.settings()->setAttribute(QWebEngineSettings::AcceleratedCompositingEnabled, accelerated);
page.setHtml("<html><body>"
"<canvas id='webgl' width='300' height='300'></canvas>"
"<script>document.getElementById('webgl').getContext('experimental-webgl')</script>"
"</body></html>");
takeScreenshot(&page);
}
void tst_QWebEnginePage::acceleratedWebGLScreenshotWithoutView()
{
webGLScreenshotWithoutView(true);
}
void tst_QWebEnginePage::unacceleratedWebGLScreenshotWithoutView()
{
webGLScreenshotWithoutView(false);
}
#endif
/**
* Test fixups for https://bugs.webkit.org/show_bug.cgi?id=30914
*
* From JS we test the following conditions.
*
* OK + QString() => SUCCESS, empty string (but not null)
* OK + "text" => SUCCESS, "text"
* CANCEL + QString() => CANCEL, null string
* CANCEL + "text" => CANCEL, null string
*/
class JSPromptPage : public QWebEnginePage {
Q_OBJECT
public:
JSPromptPage()
{}
bool javaScriptPrompt(const QUrl &securityOrigin, const QString& msg, const QString& defaultValue, QString* result)
{
if (msg == QLatin1String("test1")) {
*result = QString();
return true;
} else if (msg == QLatin1String("test2")) {
*result = QLatin1String("text");
return true;
} else if (msg == QLatin1String("test3")) {
*result = QString();
return false;
} else if (msg == QLatin1String("test4")) {
*result = QLatin1String("text");
return false;
}
qFatal("Unknown msg.");
return QWebEnginePage::javaScriptPrompt(securityOrigin, msg, defaultValue, result);
}
};
void tst_QWebEnginePage::testJSPrompt()
{
JSPromptPage page;
bool res;
QSignalSpy loadSpy(&page, SIGNAL(loadFinished(bool)));
page.setHtml(QStringLiteral("<html><body></body></html>"));
QTRY_COMPARE_WITH_TIMEOUT(loadSpy.count(), 1, 20000);
// OK + QString()
res = evaluateJavaScriptSync(&page,
"var retval = prompt('test1');"
"retval=='' && retval.length == 0;").toBool();
QVERIFY(res);
// OK + "text"
res = evaluateJavaScriptSync(&page,
"var retval = prompt('test2');"
"retval=='text' && retval.length == 4;").toBool();
QVERIFY(res);
// Cancel + QString()
res = evaluateJavaScriptSync(&page,
"var retval = prompt('test3');"
"retval===null;").toBool();
QVERIFY(res);
// Cancel + "text"
res = evaluateJavaScriptSync(&page,
"var retval = prompt('test4');"
"retval===null;").toBool();
QVERIFY(res);
}
void tst_QWebEnginePage::findText()
{
QSignalSpy loadSpy(m_view, SIGNAL(loadFinished(bool)));
m_view->setHtml(QString("<html><head></head><body><div>foo bar</div></body></html>"));
// Showing is required, otherwise all find operations fail.
m_view->show();
QTRY_COMPARE(loadSpy.count(), 1);
// Select whole page contents.
QTRY_VERIFY(m_view->page()->action(QWebEnginePage::SelectAll)->isEnabled());
m_view->page()->triggerAction(QWebEnginePage::SelectAll);
QTRY_COMPARE(m_view->hasSelection(), true);
// Invoking a stopFinding operation will not change or clear the currently selected text,
// if nothing was found beforehand.
{
CallbackSpy<bool> callbackSpy;
QSignalSpy signalSpy(m_view->page(), &QWebEnginePage::findTextFinished);
m_view->findText("", {}, callbackSpy.ref());
QVERIFY(callbackSpy.wasCalled());
QCOMPARE(signalSpy.count(), 1);
QTRY_COMPARE(m_view->selectedText(), QString("foo bar"));
}
// Invoking a startFinding operation with text that won't be found, will clear the current
// selection.
{
CallbackSpy<bool> callbackSpy;
QSignalSpy signalSpy(m_view->page(), &QWebEnginePage::findTextFinished);
m_view->findText("Will not be found", {}, callbackSpy.ref());
QCOMPARE(callbackSpy.waitForResult(), false);
QTRY_COMPARE(signalSpy.count(), 1);
auto result = signalSpy.takeFirst().value(0).value<QWebEngineFindTextResult>();
QCOMPARE(result.numberOfMatches(), 0);
QTRY_VERIFY(m_view->selectedText().isEmpty());
}
// Select whole page contents again.
m_view->page()->triggerAction(QWebEnginePage::SelectAll);
QTRY_COMPARE(m_view->hasSelection(), true);
// Invoking a startFinding operation with text that will be found, will clear the current
// selection as well.
{
CallbackSpy<bool> callbackSpy;
QSignalSpy signalSpy(m_view->page(), &QWebEnginePage::findTextFinished);
m_view->findText("foo", {}, callbackSpy.ref());
QVERIFY(callbackSpy.waitForResult());
QTRY_COMPARE(signalSpy.count(), 1);
QTRY_VERIFY(m_view->selectedText().isEmpty());
}
// Invoking a stopFinding operation after text was found, will set the selected text to the
// found text.
{
CallbackSpy<bool> callbackSpy;
QSignalSpy signalSpy(m_view->page(), &QWebEnginePage::findTextFinished);
m_view->findText("", {}, callbackSpy.ref());
QTRY_VERIFY(callbackSpy.wasCalled());
QTRY_COMPARE(signalSpy.count(), 1);
QTRY_COMPARE(m_view->selectedText(), QString("foo"));
}
// Invoking startFinding operation for the same text twice. Without any wait, the second one
// should interrupt the first one.
{
QSignalSpy signalSpy(m_view->page(), &QWebEnginePage::findTextFinished);
m_view->findText("foo", {});
m_view->findText("foo", {});
QTRY_COMPARE(signalSpy.count(), 2);
QTRY_VERIFY(m_view->selectedText().isEmpty());
QCOMPARE(signalSpy.at(0).value(0).value<QWebEngineFindTextResult>().numberOfMatches(), 0);
QCOMPARE(signalSpy.at(1).value(0).value<QWebEngineFindTextResult>().numberOfMatches(), 1);
}
}
void tst_QWebEnginePage::findTextResult()
{
QSignalSpy findTextSpy(m_view->page(), &QWebEnginePage::findTextFinished);
auto signalResult = [&findTextSpy]() -> QVector<int> {
if (findTextSpy.count() != 1)
return QVector<int>({-1, -1});
auto r = findTextSpy.takeFirst().value(0).value<QWebEngineFindTextResult>();
return QVector<int>({ r.numberOfMatches(), r.activeMatch() });
};
// findText will abort in blink if the view has an empty size.
m_view->resize(800, 600);
m_view->show();
QSignalSpy loadSpy(m_view, SIGNAL(loadFinished(bool)));
m_view->setHtml(QString("<html><head></head><body><div>foo bar</div></body></html>"));
QTRY_COMPARE(loadSpy.count(), 1);
QCOMPARE(findTextSync(m_page, ""), false);
QCOMPARE(signalResult(), QVector<int>({0, 0}));
const QStringList words = { "foo", "bar" };
for (const QString &subString : words) {
QCOMPARE(findTextSync(m_page, subString), true);
QCOMPARE(signalResult(), QVector<int>({1, 1}));
QCOMPARE(findTextSync(m_page, ""), false);
QCOMPARE(signalResult(), QVector<int>({0, 0}));
}
QCOMPARE(findTextSync(m_page, "blahhh"), false);
QCOMPARE(signalResult(), QVector<int>({0, 0}));
QCOMPARE(findTextSync(m_page, ""), false);
QCOMPARE(signalResult(), QVector<int>({0, 0}));
}
void tst_QWebEnginePage::findTextSuccessiveShouldCallAllCallbacks()
{
CallbackSpy<bool> spy1;
CallbackSpy<bool> spy2;
CallbackSpy<bool> spy3;
CallbackSpy<bool> spy4;
CallbackSpy<bool> spy5;
QSignalSpy loadSpy(m_view, SIGNAL(loadFinished(bool)));
m_view->setHtml(QString("<html><head></head><body><div>abcdefg abcdefg abcdefg abcdefg abcdefg</div></body></html>"));
QTRY_COMPARE_WITH_TIMEOUT(loadSpy.count(), 1, 20000);
m_page->findText("abcde", {}, spy1.ref());
m_page->findText("abcd", {}, spy2.ref());
m_page->findText("abc", {}, spy3.ref());
m_page->findText("ab", {}, spy4.ref());
m_page->findText("a", {}, spy5.ref());
spy5.waitForResult();
QVERIFY(spy1.wasCalled());
QVERIFY(spy2.wasCalled());
QVERIFY(spy3.wasCalled());
QVERIFY(spy4.wasCalled());
QVERIFY(spy5.wasCalled());
}
void tst_QWebEnginePage::findTextCalledOnMatch()
{
QSignalSpy loadSpy(m_view->page(), &QWebEnginePage::loadFinished);
// findText will abort in blink if the view has an empty size.
m_view->resize(800, 600);
m_view->show();
m_view->setHtml(QString("<html><head></head><body><div>foo bar</div></body></html>"));
QTRY_COMPARE(loadSpy.count(), 1);
// CALLBACK
bool callbackCalled = false;
m_view->page()->findText("foo", {}, [this, &callbackCalled](bool found) {
QVERIFY(found);
m_view->page()->findText("bar", {}, [&callbackCalled](bool found) {
QVERIFY(found);
callbackCalled = true;
});
});
QTRY_VERIFY(callbackCalled);
// SIGNAL
int findTextFinishedCount = 0;
connect(m_view->page(), &QWebEnginePage::findTextFinished, [this, &findTextFinishedCount](QWebEngineFindTextResult result) {
QCOMPARE(result.numberOfMatches(), 1);
if (findTextFinishedCount == 0)
m_view->page()->findText("bar");
findTextFinishedCount++;
});
m_view->page()->findText("foo");
QTRY_COMPARE(findTextFinishedCount, 2);
}
void tst_QWebEnginePage::findTextActiveMatchOrdinal()
{
QSignalSpy loadSpy(m_view->page(), &QWebEnginePage::loadFinished);
QSignalSpy findTextSpy(m_view->page(), &QWebEnginePage::findTextFinished);
QWebEngineFindTextResult result;
// findText will abort in blink if the view has an empty size.
m_view->resize(800, 600);
m_view->show();
m_view->setHtml(QString("<html><head></head><body><div>foo bar foo bar foo</div></body></html>"));
QTRY_COMPARE(loadSpy.count(), 1);
// Iterate over all "foo" matches.
for (int i = 1; i <= 3; ++i) {
m_view->page()->findText("foo", {});
QTRY_COMPARE(findTextSpy.count(), 1);
result = findTextSpy.takeFirst().value(0).value<QWebEngineFindTextResult>();
QCOMPARE(result.numberOfMatches(), 3);
QCOMPARE(result.activeMatch(), i);
}
// The last match is followed by the fist one.
m_view->page()->findText("foo", {});
QTRY_COMPARE(findTextSpy.count(), 1);
result = findTextSpy.takeFirst().value(0).value<QWebEngineFindTextResult>();
QCOMPARE(result.numberOfMatches(), 3);
QCOMPARE(result.activeMatch(), 1);
// The first match is preceded by the last one.
m_view->page()->findText("foo", QWebEnginePage::FindBackward);
QTRY_COMPARE(findTextSpy.count(), 1);
result = findTextSpy.takeFirst().value(0).value<QWebEngineFindTextResult>();
QCOMPARE(result.numberOfMatches(), 3);
QCOMPARE(result.activeMatch(), 3);
// Finding another word resets the activeMatch.
m_view->page()->findText("bar", {});
QTRY_COMPARE(findTextSpy.count(), 1);
result = findTextSpy.takeFirst().value(0).value<QWebEngineFindTextResult>();
QCOMPARE(result.numberOfMatches(), 2);
QCOMPARE(result.activeMatch(), 1);
// If no match activeMatch is 0.
m_view->page()->findText("bla", {});
QTRY_COMPARE(findTextSpy.count(), 1);
result = findTextSpy.takeFirst().value(0).value<QWebEngineFindTextResult>();
QCOMPARE(result.numberOfMatches(), 0);
QCOMPARE(result.activeMatch(), 0);
}
static QWindow *findNewTopLevelWindow(const QWindowList &oldTopLevelWindows)
{
const auto tlws = QGuiApplication::topLevelWindows();
for (auto w : tlws) {
if (!oldTopLevelWindows.contains(w)) {
return w;
}
}
return nullptr;
}
void tst_QWebEnginePage::comboBoxPopupPositionAfterMove()
{
QWebEngineView view;
view.move(QGuiApplication::primaryScreen()->availableGeometry().topLeft());
view.resize(640, 480);
view.show();
QSignalSpy loadSpy(&view, SIGNAL(loadFinished(bool)));
view.setHtml(QLatin1String("<html><head></head><body><select id='foo'>"
"<option>fran</option><option>troz</option>"
"</select></body></html>"));
QTRY_COMPARE(loadSpy.count(), 1);
const auto oldTlws = QGuiApplication::topLevelWindows();
QWindow *window = view.windowHandle();
QTest::mouseClick(window, Qt::LeftButton, Qt::KeyboardModifiers(),
elementCenter(view.page(), "foo"));
QWindow *popup = nullptr;
QTRY_VERIFY(popup = findNewTopLevelWindow(oldTlws));
QTRY_VERIFY(QGuiApplication::topLevelWindows().contains(popup));
QTRY_VERIFY(!popup->position().isNull());
QPoint popupPos = popup->position();
// Close the popup by clicking somewhere into the page.
QTest::mouseClick(window, Qt::LeftButton, Qt::KeyboardModifiers(), QPoint(1, 1));
QTRY_VERIFY(!QGuiApplication::topLevelWindows().contains(popup));
auto jsViewPosition = [&view]() {
QLatin1String script("(function() { return [window.screenX, window.screenY]; })()");
QVariantList posList = evaluateJavaScriptSync(view.page(), script).toList();
if (posList.count() != 2) {
qWarning("jsViewPosition failed.");
return QPoint();
}
return QPoint(posList.at(0).toInt(), posList.at(1).toInt());
};
// Move the top-level QWebEngineView a little and check the popup's position.
const QPoint offset(12, 13);
view.move(view.pos() + offset);
QTRY_COMPARE(jsViewPosition(), view.pos());
QTest::mouseClick(window, Qt::LeftButton, Qt::KeyboardModifiers(),
elementCenter(view.page(), "foo"));
QTRY_VERIFY(popup = findNewTopLevelWindow(oldTlws));
QTRY_VERIFY(QGuiApplication::topLevelWindows().contains(popup));
QTRY_VERIFY(!popup->position().isNull());
QCOMPARE(popupPos + offset, popup->position());
QTest::mouseClick(window, Qt::LeftButton, Qt::KeyboardModifiers(), QPoint(1, 1));
QTRY_VERIFY(!QGuiApplication::topLevelWindows().contains(popup));
}
void tst_QWebEnginePage::comboBoxPopupPositionAfterChildMove()
{
QWidget mainWidget;
mainWidget.setLayout(new QHBoxLayout);
QWidget spacer;
mainWidget.layout()->addWidget(&spacer);
QWebEngineView view;
mainWidget.layout()->addWidget(&view);
QScreen *screen = QGuiApplication::primaryScreen();
mainWidget.move(screen->availableGeometry().topLeft());
mainWidget.resize(640, 480);
mainWidget.show();
QSignalSpy loadSpy(&view, SIGNAL(loadFinished(bool)));
view.setHtml(QLatin1String("<html><head></head><body><select autofocus id='foo'>"
"<option value=\"narf\">narf</option><option>zort</option>"
"</select></body></html>"));
QTRY_COMPARE(loadSpy.count(), 1);
const auto oldTlws = QGuiApplication::topLevelWindows();
QWindow *window = view.window()->windowHandle();
QTest::mouseClick(window, Qt::LeftButton, Qt::KeyboardModifiers(),
view.mapTo(view.window(), elementCenter(view.page(), "foo")));
QWindow *popup = nullptr;
QTRY_VERIFY(popup = findNewTopLevelWindow(oldTlws));
QTRY_VERIFY(QGuiApplication::topLevelWindows().contains(popup));
QTRY_VERIFY(!popup->position().isNull());
QPoint popupPos = popup->position();
// Close the popup by clicking somewhere into the page.
QTest::mouseClick(window, Qt::LeftButton, Qt::KeyboardModifiers(),
view.mapTo(view.window(), QPoint(1, 1)));
QTRY_VERIFY(!QGuiApplication::topLevelWindows().contains(popup));
int originalViewWidth = view.size().width();
auto jsViewWidth = [&view]() {
QLatin1String script("(function() { return window.innerWidth; })()");
int viewWidth = evaluateJavaScriptSync(view.page(), script).toInt();
return viewWidth;
};
// Resize the "spacer" widget, and implicitly change the global position of the QWebEngineView.
const int offset = 50;
spacer.setMinimumWidth(spacer.size().width() + offset);
QTRY_COMPARE(jsViewWidth(), originalViewWidth - offset);
QTest::mouseClick(window, Qt::LeftButton, Qt::KeyboardModifiers(),
view.mapTo(view.window(), elementCenter(view.page(), "foo")));
QTRY_VERIFY(popup = findNewTopLevelWindow(oldTlws));
QTRY_VERIFY(!popup->position().isNull());
QCOMPARE(popupPos + QPoint(50, 0), popup->position());
}
#ifdef Q_OS_MAC
void tst_QWebEnginePage::macCopyUnicodeToClipboard()
{
QString unicodeText = QString::fromUtf8("αβγδεζηθικλμπ");
m_page->setHtml(QString("<html><head><meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" /></head><body>%1</body></html>").arg(unicodeText));
m_page->triggerAction(QWebEnginePage::SelectAll);
m_page->triggerAction(QWebEnginePage::Copy);
QString clipboardData = QString::fromUtf8(QApplication::clipboard()->mimeData()->data(QLatin1String("text/html")));
QVERIFY(clipboardData.contains(QLatin1String("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />")));
QVERIFY(clipboardData.contains(unicodeText));
}
#endif
void tst_QWebEnginePage::deleteQWebEngineViewTwice()
{
for (int i = 0; i < 2; ++i) {
QMainWindow mainWindow;
QWebEngineView* webView = new QWebEngineView(&mainWindow);
mainWindow.setCentralWidget(webView);
webView->load(QUrl("qrc:///resources/frame_a.html"));
mainWindow.show();
QSignalSpy spyFinished(webView, &QWebEngineView::loadFinished);
QVERIFY(spyFinished.wait());
}
}
class SpyForLoadSignalsOrder : public QStateMachine {
Q_OBJECT
public:
SpyForLoadSignalsOrder(QWebEnginePage* page, QObject* parent = 0)
: QStateMachine(parent)
{
connect(page, SIGNAL(loadProgress(int)), SLOT(onLoadProgress(int)));
QState* waitingForLoadStarted = new QState(this);
QState* waitingForFirstLoadProgress = new QState(this);
QState* waitingForLastLoadProgress = new QState(this);
QState* waitingForLoadFinished = new QState(this);
QFinalState* final = new QFinalState(this);
waitingForLoadStarted->addTransition(page, SIGNAL(loadStarted()), waitingForFirstLoadProgress);
waitingForFirstLoadProgress->addTransition(this, SIGNAL(firstLoadProgress()), waitingForLastLoadProgress);
waitingForLastLoadProgress->addTransition(this, SIGNAL(lastLoadProgress()), waitingForLoadFinished);
waitingForLoadFinished->addTransition(page, SIGNAL(loadFinished(bool)), final);
setInitialState(waitingForLoadStarted);
start();
}
bool isFinished() const
{
return !isRunning();
}
public Q_SLOTS:
void onLoadProgress(int progress)
{
if (progress == 0)
emit firstLoadProgress();
else if (progress == 100)
emit lastLoadProgress();
}
Q_SIGNALS:
void firstLoadProgress();
void lastLoadProgress();
};
void tst_QWebEnginePage::loadSignalsOrder_data()
{
QTest::addColumn<QUrl>("url");
QTest::newRow("inline data") << QUrl("data:text/html,This is first page");
QTest::newRow("simple page") << QUrl("qrc:///resources/content.html");
QTest::newRow("frameset page") << QUrl("qrc:///resources/index.html");
}
void tst_QWebEnginePage::loadSignalsOrder()
{
QFETCH(QUrl, url);
QWebEnginePage page;
SpyForLoadSignalsOrder loadSpy(&page);
QSignalSpy spyLoadSpy(&loadSpy, &SpyForLoadSignalsOrder::started);
QVERIFY(spyLoadSpy.wait(500));
page.load(url);
QTRY_VERIFY_WITH_TIMEOUT(loadSpy.isFinished(), 20000);
}
void tst_QWebEnginePage::renderWidgetHostViewNotShowTopLevel()
{
QWebEnginePage page;
QSignalSpy spyLoadFinished(&page, SIGNAL(loadFinished(bool)));
page.load(QUrl("http://qt-project.org"));
if (!spyLoadFinished.wait(10000) || !spyLoadFinished.at(0).at(0).toBool())
QSKIP("Couldn't load page from network, skipping test.");
spyLoadFinished.clear();
// Loading a different domain will force the creation of a separate render
// process and should therefore create a new RenderWidgetHostViewQtDelegateWidget.
page.load(QUrl("http://www.wikipedia.org/"));
if (!spyLoadFinished.wait(10000) || !spyLoadFinished.at(0).at(0).toBool())
QSKIP("Couldn't load page from network, skipping test.");
// Make sure that RenderWidgetHostViewQtDelegateWidgets are not shown as top-level.
// They should only be made visible when parented to a QWebEngineView.
const QList<QWidget *> widgets = QApplication::topLevelWidgets();
for (QWidget *widget : widgets)
QCOMPARE(widget->isVisible(), false);
}
class GetUserMediaTestPage : public QWebEnginePage {
Q_OBJECT
public:
GetUserMediaTestPage()
: m_gotRequest(false)
, m_loadSucceeded(false)
{
connect(this, &QWebEnginePage::featurePermissionRequested, this, &GetUserMediaTestPage::onFeaturePermissionRequested);
connect(this, &QWebEnginePage::loadFinished, [this](bool success){
m_loadSucceeded = success;
});
// We need to load content from a resource in order for the securityOrigin to be valid.
load(QUrl("qrc:///resources/content.html"));
}
void jsGetMedia(const QString &call)
{
evaluateJavaScriptSync(this,
QStringLiteral(
"var promiseFulfilled = false;"
"var promiseRejected = false;"
"navigator.mediaDevices.%1"
".then(stream => { promiseFulfilled = true})"
".catch(err => { promiseRejected = true})")
.arg(call));
}
void jsGetUserMedia(const QString &constraints)
{
jsGetMedia(QStringLiteral("getUserMedia(%1)").arg(constraints));
}
bool jsPromiseFulfilled()
{
return evaluateJavaScriptSync(this, QStringLiteral("promiseFulfilled")).toBool();
}
bool jsPromiseRejected()
{
return evaluateJavaScriptSync(this, QStringLiteral("promiseRejected")).toBool();
}
void rejectPendingRequest()
{
setFeaturePermission(m_requestSecurityOrigin, m_requestedFeature, QWebEnginePage::PermissionDeniedByUser);
m_gotRequest = false;
}
void acceptPendingRequest()
{
setFeaturePermission(m_requestSecurityOrigin, m_requestedFeature, QWebEnginePage::PermissionGrantedByUser);
m_gotRequest = false;
}
bool gotFeatureRequest(QWebEnginePage::Feature feature)
{
return m_gotRequest && m_requestedFeature == feature;
}
bool gotFeatureRequest() const
{
return m_gotRequest;
}
bool loadSucceeded() const
{
return m_loadSucceeded;
}
private Q_SLOTS:
void onFeaturePermissionRequested(const QUrl &securityOrigin, QWebEnginePage::Feature feature)
{
m_requestedFeature = feature;
m_requestSecurityOrigin = securityOrigin;
m_gotRequest = true;
}
private:
bool m_gotRequest;
bool m_loadSucceeded;
QWebEnginePage::Feature m_requestedFeature;
QUrl m_requestSecurityOrigin;
};
void tst_QWebEnginePage::getUserMediaRequest_data()
{
QTest::addColumn<QString>("call");
QTest::addColumn<QWebEnginePage::Feature>("feature");
QTest::addRow("device audio")
<< "getUserMedia({audio: true})" << QWebEnginePage::MediaAudioCapture;
QTest::addRow("device video")
<< "getUserMedia({video: true})" << QWebEnginePage::MediaVideoCapture;
QTest::addRow("device audio+video")
<< "getUserMedia({audio: true, video: true})" << QWebEnginePage::MediaAudioVideoCapture;
QTest::addRow("desktop video")
<< "getUserMedia({video: { mandatory: { chromeMediaSource: 'desktop' }}})"
<< QWebEnginePage::DesktopVideoCapture;
QTest::addRow("desktop audio+video")
<< "getUserMedia({audio: { mandatory: { chromeMediaSource: 'desktop' }}, video: { mandatory: { chromeMediaSource: 'desktop' }}})"
<< QWebEnginePage::DesktopAudioVideoCapture;
QTest::addRow("display video")
<< "getDisplayMedia()" << QWebEnginePage::DesktopVideoCapture;
}
void tst_QWebEnginePage::getUserMediaRequest()
{
QFETCH(QString, call);
QFETCH(QWebEnginePage::Feature, feature);
GetUserMediaTestPage page;
QWebEngineView view;
if (feature == QWebEnginePage::DesktopVideoCapture || feature == QWebEnginePage::DesktopAudioVideoCapture) {
// Desktop capture needs to be on a desktop.
view.setPage(&page);
view.resize(640, 480);
view.show();
QVERIFY(QTest::qWaitForWindowExposed(&view));
}
QTRY_VERIFY_WITH_TIMEOUT(page.loadSucceeded(), 60000);
page.settings()->setAttribute(QWebEngineSettings::ScreenCaptureEnabled, true);
// 1. Rejecting request on C++ side should reject promise on JS side.
page.jsGetMedia(call);
QTRY_VERIFY(page.gotFeatureRequest(feature));
page.rejectPendingRequest();
QTRY_VERIFY(!page.jsPromiseFulfilled() && page.jsPromiseRejected());
// 2. Accepting request on C++ side should either fulfill or reject the
// Promise on JS side. Due to the potential lack of physical media devices
// deeper in the content layer we cannot guarantee that the promise will
// always be fulfilled, however in this case an error should be returned to
// JS instead of leaving the Promise in limbo.
page.jsGetMedia(call);
QTRY_VERIFY(page.gotFeatureRequest(feature));
page.acceptPendingRequest();
QTRY_VERIFY(page.jsPromiseFulfilled() || page.jsPromiseRejected());
// 3. Media feature permissions are not remembered.
page.jsGetMedia(call);
QTRY_VERIFY(page.gotFeatureRequest(feature));
page.acceptPendingRequest();
QTRY_VERIFY(page.jsPromiseFulfilled() || page.jsPromiseRejected());
}
void tst_QWebEnginePage::getUserMediaRequestDesktopAudio()
{
GetUserMediaTestPage page;
QTRY_VERIFY_WITH_TIMEOUT(page.loadSucceeded(), 20000);
page.settings()->setAttribute(QWebEngineSettings::ScreenCaptureEnabled, true);
// Audio-only desktop capture is not supported. JS Promise should be
// rejected immediately.
page.jsGetUserMedia(
QStringLiteral("{audio: { mandatory: { chromeMediaSource: 'desktop' }}}"));
QTRY_VERIFY(!page.jsPromiseFulfilled() && page.jsPromiseRejected());
page.jsGetUserMedia(
QStringLiteral("{audio: { mandatory: { chromeMediaSource: 'desktop' }}, video: true}"));
QTRY_VERIFY(!page.jsPromiseFulfilled() && page.jsPromiseRejected());
}
void tst_QWebEnginePage::getUserMediaRequestSettingDisabled()
{
GetUserMediaTestPage page;
QTRY_VERIFY_WITH_TIMEOUT(page.loadSucceeded(), 20000);
page.settings()->setAttribute(QWebEngineSettings::ScreenCaptureEnabled, false);
// With the setting disabled, the JS Promise should be rejected without
// asking for permission first.
page.jsGetUserMedia(QStringLiteral("{video: { mandatory: { chromeMediaSource: 'desktop' }}}"));
QTRY_VERIFY(!page.jsPromiseFulfilled() && page.jsPromiseRejected());
}
// Try to trigger any possible race condition between the UI thread (permission
// management) and the audio/device thread (desktop capture initialization).
void tst_QWebEnginePage::getUserMediaRequestDesktopVideoManyPages()
{
const QString constraints = QStringLiteral("{video: { mandatory: { chromeMediaSource: 'desktop' }}}");
const QWebEnginePage::Feature feature = QWebEnginePage::DesktopVideoCapture;
std::vector<GetUserMediaTestPage> pages(10);
// Desktop capture needs to be on a desktop
std::vector<QWebEngineView> views(10);
for (size_t i = 0; i < views.size(); ++i) {
QWebEngineView *view = &(views[i]);
GetUserMediaTestPage *page = &(pages[i]);
view->setPage(page);
view->resize(640, 480);
view->show();
QVERIFY(QTest::qWaitForWindowExposed(view));
}
for (GetUserMediaTestPage &page : pages)
QTRY_VERIFY_WITH_TIMEOUT(page.loadSucceeded(), 20000);
for (GetUserMediaTestPage &page : pages)
page.settings()->setAttribute(QWebEngineSettings::ScreenCaptureEnabled, true);
for (GetUserMediaTestPage &page : pages)
page.jsGetUserMedia(constraints);
for (GetUserMediaTestPage &page : pages)
QTRY_VERIFY(page.gotFeatureRequest(feature));
for (GetUserMediaTestPage &page : pages)
page.acceptPendingRequest();
for (GetUserMediaTestPage &page : pages)
QTRY_VERIFY(page.jsPromiseFulfilled() || page.jsPromiseRejected());
}
// Try to trigger any possible race condition between the UI or audio/device
// threads and the desktop capture thread, where the capture actually happens.
void tst_QWebEnginePage::getUserMediaRequestDesktopVideoManyRequests()
{
const QString constraints = QStringLiteral("{video: { mandatory: { chromeMediaSource: 'desktop' }}}");
const QWebEnginePage::Feature feature = QWebEnginePage::DesktopVideoCapture;
GetUserMediaTestPage page;
// Desktop capture needs to be on a desktop
QWebEngineView view;
view.setPage(&page);
view.resize(640, 480);
view.show();
QVERIFY(QTest::qWaitForWindowExposed(&view));
QTRY_VERIFY_WITH_TIMEOUT(page.loadSucceeded(), 20000);
page.settings()->setAttribute(QWebEngineSettings::ScreenCaptureEnabled, true);
for (int i = 0; i != 100; ++i) {
page.jsGetUserMedia(constraints);
QTRY_VERIFY(page.gotFeatureRequest(feature));
page.acceptPendingRequest();
QTRY_VERIFY(page.jsPromiseFulfilled() || page.jsPromiseRejected());
}
}
void tst_QWebEnginePage::savePage()
{
QWebEngineView view;
QWebEnginePage *page = view.page();
connect(page->profile(), &QWebEngineProfile::downloadRequested,
[] (QWebEngineDownloadItem *item)
{
connect(item, &QWebEngineDownloadItem::finished,
&QTestEventLoop::instance(), &QTestEventLoop::exitLoop, Qt::QueuedConnection);
});
const QString urlPrefix = QStringLiteral("data:text/html,<h1>");
const QString text = QStringLiteral("There is Thingumbob shouting!");
page->load(QUrl(urlPrefix + text));
QSignalSpy spyFinished(page, &QWebEnginePage::loadFinished);
QVERIFY(spyFinished.wait());
QCOMPARE(toPlainTextSync(page), text);
// Save the loaded page as HTML.
QTemporaryDir tempDir(QDir::tempPath() + "/tst_qwebengineview-XXXXXX");
const QString filePath = tempDir.path() + "/thingumbob.html";
page->save(filePath, QWebEngineDownloadItem::CompleteHtmlSaveFormat);
QTestEventLoop::instance().enterLoop(10);
// Load something else.
page->load(QUrl(urlPrefix + QLatin1String("It's a Snark!")));
QVERIFY(spyFinished.wait());
QVERIFY(toPlainTextSync(page) != text);
// Load the saved page and compare the contents.
page->load(QUrl::fromLocalFile(filePath));
QVERIFY(spyFinished.wait());
QCOMPARE(toPlainTextSync(page), text);
}
void tst_QWebEnginePage::openWindowDefaultSize()
{
TestPage page;
QSignalSpy windowCreatedSpy(&page, SIGNAL(windowCreated()));
QWebEngineView view;
page.setView(&view);
view.show();
page.settings()->setAttribute(QWebEngineSettings::JavascriptCanOpenWindows, true);
// Open a default window.
page.runJavaScript("window.open()");
QTRY_COMPARE(windowCreatedSpy.count(), 1);
// Open a too small window.
evaluateJavaScriptSync(&page, "window.open('','about:blank','width=10,height=10')");
QTRY_COMPARE(windowCreatedSpy.count(), 2);
// The number of popups created should be two.
QCOMPARE(page.createdWindows.size(), 2);
QRect requestedGeometry = page.createdWindows[0]->requestedGeometry;
// Check default size has been requested.
QCOMPARE(requestedGeometry.width(), 0);
QCOMPARE(requestedGeometry.height(), 0);
requestedGeometry = page.createdWindows[1]->requestedGeometry;
// Check minimum size has been requested.
QCOMPARE(requestedGeometry.width(), 100);
QCOMPARE(requestedGeometry.height(), 100);
}
bool tst_QWebEnginePage::isFalseJavaScriptResult(QWebEnginePage *page, const QString &javaScript)
{
QVariant result = evaluateJavaScriptSync(page, javaScript);
return !result.isNull() && result.isValid() && result == QVariant(false);
}
bool tst_QWebEnginePage::isTrueJavaScriptResult(QWebEnginePage *page, const QString &javaScript)
{
QVariant result = evaluateJavaScriptSync(page, javaScript);
return !result.isNull() && result.isValid() && result == QVariant(true);
}
bool tst_QWebEnginePage::isEmptyListJavaScriptResult(QWebEnginePage *page, const QString &javaScript)
{
QVariant result = evaluateJavaScriptSync(page, javaScript);
return !result.isNull() && result.isValid() && result == QList<QVariant>();
}
void tst_QWebEnginePage::runJavaScript()
{
TestPage page;
QVariant result;
QVariantMap map;
QVERIFY(isFalseJavaScriptResult(&page, "false"));
QCOMPARE(evaluateJavaScriptSync(&page, "2").toInt(), 2);
QCOMPARE(evaluateJavaScriptSync(&page, "2.5").toDouble(), 2.5);
QCOMPARE(evaluateJavaScriptSync(&page, "\"Test\"").toString(), "Test");
QVERIFY(isEmptyListJavaScriptResult(&page, "[]"));
map.insert(QStringLiteral("test"), QVariant(2));
QCOMPARE(evaluateJavaScriptSync(&page, "var el = {\"test\": 2}; el").toMap(), map);
QVERIFY(evaluateJavaScriptSync(&page, "null").isNull());
result = evaluateJavaScriptSync(&page, "undefined");
QVERIFY(result.isNull() && !result.isValid());
QCOMPARE(evaluateJavaScriptSync(&page, "new Date(42000)").toDate(), QVariant(42.0).toDate());
QCOMPARE(evaluateJavaScriptSync(&page, "new ArrayBuffer(8)").toByteArray(), QByteArray(8, 0));
result = evaluateJavaScriptSync(&page, "(function(){})");
QVERIFY(result.isNull() && !result.isValid());
QCOMPARE(evaluateJavaScriptSync(&page, "new Promise(function(){})"), QVariant(QVariantMap{}));
}
void tst_QWebEnginePage::runJavaScriptDisabled()
{
QWebEnginePage page;
QSignalSpy spy(&page, &QWebEnginePage::loadFinished);
page.settings()->setAttribute(QWebEngineSettings::JavascriptEnabled, false);
// Settings changes take effect asynchronously. The load and wait ensure
// that the settings are applied by the time we start to execute JavaScript.
page.load(QStringLiteral("about:blank"));
QTRY_COMPARE_WITH_TIMEOUT(spy.count(), 1, 20000);
QCOMPARE(evaluateJavaScriptSyncInWorld(&page, QStringLiteral("1+1"), QWebEngineScript::MainWorld),
QVariant());
QCOMPARE(evaluateJavaScriptSyncInWorld(&page, QStringLiteral("1+1"), QWebEngineScript::ApplicationWorld),
QVariant(2));
}
// Based on https://bugreports.qt.io/browse/QTBUG-73876
void tst_QWebEnginePage::runJavaScriptFromSlot()
{
QWebEngineProfile profile;
QWebEnginePage page(&profile);
QSignalSpy loadFinishedSpy(&page, &QWebEnginePage::loadFinished);
page.setHtml("<html><body>"
" <input type='text' id='input1' value='QtWebEngine' size='50' />"
"</body></html>");
QTRY_COMPARE(loadFinishedSpy.count(), 1);
bool done = false;
connect(&page, &QWebEnginePage::selectionChanged, [&]() {
QTRY_COMPARE(evaluateJavaScriptSync(&page, QStringLiteral("2+2")), QVariant(4));
done = true;
});
evaluateJavaScriptSync(&page, QStringLiteral("const input = document.getElementById('input1');"
"input.focus();"
"input.select();"));
QTRY_VERIFY(done);
}
void tst_QWebEnginePage::fullScreenRequested()
{
QWebEngineView view;
QWebEnginePage* page = view.page();
view.show();
page->settings()->setAttribute(QWebEngineSettings::FullScreenSupportEnabled, true);
QSignalSpy loadSpy(&view, SIGNAL(loadFinished(bool)));
page->load(QUrl("qrc:///resources/fullscreen.html"));
QTRY_COMPARE(loadSpy.count(), 1);
QTRY_VERIFY(isTrueJavaScriptResult(page, "document.webkitFullscreenEnabled"));
QVERIFY(isFalseJavaScriptResult(page, "document.webkitIsFullScreen"));
// FullscreenRequest must be a user gesture
bool acceptRequest = true;
connect(page, &QWebEnginePage::fullScreenRequested,
[&acceptRequest](QWebEngineFullScreenRequest request) {
if (acceptRequest) request.accept(); else request.reject();
});
QTest::keyPress(view.focusProxy(), Qt::Key_Space);
QTRY_VERIFY(isTrueJavaScriptResult(page, "document.webkitIsFullScreen"));
page->runJavaScript("document.webkitExitFullscreen()");
QTRY_VERIFY(isFalseJavaScriptResult(page, "document.webkitIsFullScreen"));
acceptRequest = false;
QVERIFY(isTrueJavaScriptResult(page, "document.webkitFullscreenEnabled"));
QTest::keyPress(view.focusProxy(), Qt::Key_Space);
QTRY_VERIFY(isFalseJavaScriptResult(page, "document.webkitIsFullScreen"));
}
void tst_QWebEnginePage::quotaRequested()
{
ConsolePage page;
QWebEngineView view;
view.setPage(&page);
QSignalSpy loadFinishedSpy(&page, SIGNAL(loadFinished(bool)));
page.load(QUrl("qrc:///resources/content.html"));
QVERIFY(loadFinishedSpy.wait());
connect(&page, &QWebEnginePage::quotaRequested,
[] (QWebEngineQuotaRequest request)
{
if (request.requestedSize() <= 5000)
request.accept();
else
request.reject();
});
evaluateJavaScriptSync(&page,
"navigator.webkitPersistentStorage.requestQuota(1024, function(grantedSize) {" \
"console.log(grantedSize);" \
"});");
QTRY_COMPARE(page.messages.count(), 1);
QTRY_COMPARE(page.messages[0], QString("1024"));
evaluateJavaScriptSync(&page,
"navigator.webkitPersistentStorage.requestQuota(6000, function(grantedSize) {" \
"console.log(grantedSize);" \
"});");
QTRY_COMPARE(page.messages.count(), 2);
QTRY_COMPARE(page.messages[1], QString("1024"));
evaluateJavaScriptSync(&page,
"navigator.webkitPersistentStorage.queryUsageAndQuota(function(usedBytes, grantedBytes) {" \
"console.log(usedBytes + ', ' + grantedBytes);" \
"});");
QTRY_COMPARE(page.messages.count(), 3);
QTRY_COMPARE(page.messages[2], QString("0, 1024"));
}
void tst_QWebEnginePage::symmetricUrl()
{
QWebEngineView view;
QSignalSpy loadFinishedSpy(view.page(), SIGNAL(loadFinished(bool)));
QVERIFY(view.url().isEmpty());
QCOMPARE(view.history()->count(), 0);
QUrl dataUrl("data:text/html,<h1>Test");
view.setUrl(dataUrl);
QCOMPARE(view.url(), dataUrl);
QCOMPARE(view.history()->count(), 0);
// loading is _not_ immediate, so the text isn't set just yet.
QVERIFY(toPlainTextSync(view.page()).isEmpty());
QTRY_COMPARE_WITH_TIMEOUT(loadFinishedSpy.count(), 1, 20000);
QCOMPARE(view.history()->count(), 1);
QCOMPARE(toPlainTextSync(view.page()), QString("Test"));
QUrl dataUrl2("data:text/html,<h1>Test2");
QUrl dataUrl3("data:text/html,<h1>Test3");
view.setUrl(dataUrl2);
view.setUrl(dataUrl3);
QCOMPARE(view.url(), dataUrl3);
// setUrl(dataUrl3) might override the pending load for dataUrl2. Or not.
QTRY_VERIFY(loadFinishedSpy.count() >= 2);
QTRY_VERIFY(loadFinishedSpy.count() <= 3);
// setUrl(dataUrl3) might stop Chromium from adding a navigation entry for dataUrl2,
// depending on whether the load of dataUrl2 could be completed in time.
QVERIFY(view.history()->count() >= 2);
QVERIFY(view.history()->count() <= 3);
QCOMPARE(toPlainTextSync(view.page()), QString("Test3"));
}
void tst_QWebEnginePage::progressSignal()
{
QSignalSpy progressSpy(m_view, SIGNAL(loadProgress(int)));
QUrl dataUrl("data:text/html,<h1>Test");
m_view->setUrl(dataUrl);
QSignalSpy spyFinished(m_view, &QWebEngineView::loadFinished);
QVERIFY(spyFinished.wait());
QVERIFY(progressSpy.size() >= 2);
int previousValue = -1;
for (QSignalSpy::ConstIterator it = progressSpy.begin(); it < progressSpy.end(); ++it) {
int current = (*it).first().toInt();
// verbose output for faulty condition
if (!(current >= previousValue)) {
qDebug() << "faulty progress values:";
for (QSignalSpy::ConstIterator it2 = progressSpy.begin(); it2 < progressSpy.end(); ++it2)
qDebug() << (*it2).first().toInt();
QVERIFY(current >= previousValue);
}
previousValue = current;
}
// But we always end at 100%
QCOMPARE(progressSpy.last().first().toInt(), 100);
}
void tst_QWebEnginePage::urlChange()
{
QSignalSpy urlSpy(m_page, &QWebEnginePage::urlChanged);
QUrl dataUrl("data:text/html,<h1>Test");
m_view->setUrl(dataUrl);
QTRY_COMPARE(urlSpy.size(), 1);
QCOMPARE(urlSpy.takeFirst().value(0).toUrl(), dataUrl);
QUrl dataUrl2("data:text/html,<html><head><title>title</title></head><body><h1>Test</body></html>");
m_view->setUrl(dataUrl2);
QTRY_COMPARE(urlSpy.size(), 1);
QCOMPARE(urlSpy.takeFirst().value(0).toUrl(), dataUrl2);
QUrl testUrl("http://test.qt.io/");
m_view->setHtml(QStringLiteral("<h1>Test</h1"), testUrl);
QTRY_COMPARE(urlSpy.size(), 1);
QCOMPARE(urlSpy.takeFirst().value(0).toUrl(), testUrl);
}
class FakeReply : public QNetworkReply {
Q_OBJECT
public:
static const QUrl urlFor404ErrorWithoutContents;
FakeReply(const QNetworkRequest& request, QObject* parent = 0)
: QNetworkReply(parent)
{
setOperation(QNetworkAccessManager::GetOperation);
setRequest(request);
setUrl(request.url());
if (request.url() == QUrl("qrc:/test1.html")) {
setHeader(QNetworkRequest::LocationHeader, QString("qrc:/test2.html"));
setAttribute(QNetworkRequest::RedirectionTargetAttribute, QUrl("qrc:/test2.html"));
QTimer::singleShot(0, this, SLOT(continueRedirect()));
}
#ifndef QT_NO_OPENSSL
else if (request.url() == QUrl("qrc:/fake-ssl-error.html")) {
setError(QNetworkReply::SslHandshakeFailedError, tr("Fake error!"));
QTimer::singleShot(0, this, SLOT(continueError()));
}
#endif
else if (request.url().host() == QLatin1String("abcdef.abcdef")) {
setError(QNetworkReply::HostNotFoundError, tr("Invalid URL"));
QTimer::singleShot(0, this, SLOT(continueError()));
} else if (request.url() == FakeReply::urlFor404ErrorWithoutContents) {
setError(QNetworkReply::ContentNotFoundError, "Not found");
setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 404);
QTimer::singleShot(0, this, SLOT(continueError()));
}
open(QIODevice::ReadOnly);
}
~FakeReply()
{
close();
}
virtual void abort() {}
virtual void close() {}
protected:
qint64 readData(char*, qint64)
{
return 0;
}
private Q_SLOTS:
void continueRedirect()
{
emit metaDataChanged();
emit finished();
}
void continueError()
{
emit error(this->error());
emit finished();
}
};
const QUrl FakeReply::urlFor404ErrorWithoutContents = QUrl("http://this.will/return-http-404-error-without-contents.html");
class FakeNetworkManager : public QNetworkAccessManager {
Q_OBJECT
public:
FakeNetworkManager(QObject* parent) : QNetworkAccessManager(parent) { }
protected:
virtual QNetworkReply* createRequest(Operation op, const QNetworkRequest& request, QIODevice* outgoingData)
{
QString url = request.url().toString();
if (op == QNetworkAccessManager::GetOperation) {
#ifndef QT_NO_OPENSSL
if (url == "qrc:/fake-ssl-error.html") {
FakeReply* reply = new FakeReply(request, this);
QList<QSslError> errors;
emit sslErrors(reply, errors << QSslError(QSslError::UnspecifiedError));
return reply;
}
#endif
if (url == "qrc:/test1.html" || url == "http://abcdef.abcdef/" || request.url() == FakeReply::urlFor404ErrorWithoutContents)
return new FakeReply(request, this);
}
return QNetworkAccessManager::createRequest(op, request, outgoingData);
}
};
void tst_QWebEnginePage::requestedUrlAfterSetAndLoadFailures()
{
QWebEnginePage page;
page.settings()->setAttribute(QWebEngineSettings::ErrorPageEnabled, false);
QSignalSpy spy(&page, SIGNAL(loadFinished(bool)));
const QUrl first("http://abcdef.abcdef/");
page.setUrl(first);
QTRY_COMPARE_WITH_TIMEOUT(spy.count(), 1, 20000);
QCOMPARE(page.url(), first);
QCOMPARE(page.requestedUrl(), first);
QVERIFY(!spy.at(0).first().toBool());
const QUrl second("http://abcdef.abcdef/another_page.html");
QVERIFY(first != second);
page.load(second);
QTRY_COMPARE_WITH_TIMEOUT(spy.count(), 2, 20000);
QCOMPARE(page.url(), first);
QCOMPARE(page.requestedUrl(), second);
QVERIFY(!spy.at(1).first().toBool());
}
void tst_QWebEnginePage::asyncAndDelete()
{
QScopedPointer<QWebEnginePage> page(new QWebEnginePage);
CallbackSpy<QString> plainTextSpy;
CallbackSpy<QString> htmlSpy;
page->toPlainText(plainTextSpy.ref());
page->toHtml(htmlSpy.ref());
page.reset();
// Pending callbacks should be called with an empty value in the page's destructor.
QCOMPARE(plainTextSpy.waitForResult(), QString());
QVERIFY(plainTextSpy.wasCalled());
QCOMPARE(htmlSpy.waitForResult(), QString());
QVERIFY(htmlSpy.wasCalled());
}
void tst_QWebEnginePage::earlyToHtml()
{
QString html("<html><head></head><body></body></html>");
QCOMPARE(toHtmlSync(m_view->page()), html);
}
void tst_QWebEnginePage::setHtml()
{
QString html("<html><head></head><body><p>hello world</p></body></html>");
QSignalSpy spy(m_view->page(), SIGNAL(loadFinished(bool)));
m_view->page()->setHtml(html);
QVERIFY(spy.wait());
QCOMPARE(toHtmlSync(m_view->page()), html);
}
void tst_QWebEnginePage::setHtmlWithImageResource()
{
// We allow access to qrc resources from any security origin, including local and anonymous
QLatin1String html("<html><body><p>hello world</p><img src='qrc:/resources/image.png'/></body></html>");
QWebEnginePage page;
QSignalSpy spy(&page, SIGNAL(loadFinished(bool)));
page.setHtml(html, QUrl("file:///path/to/file"));
QTRY_COMPARE_WITH_TIMEOUT(spy.count(), 1, 12000);
QCOMPARE(evaluateJavaScriptSync(&page, "document.images.length").toInt(), 1);
QCOMPARE(evaluateJavaScriptSync(&page, "document.images[0].width").toInt(), 128);
QCOMPARE(evaluateJavaScriptSync(&page, "document.images[0].height").toInt(), 128);
// Now we test the opposite: without a baseUrl as a local file, we can still request qrc resources.
page.setHtml(html);
QTRY_COMPARE(spy.count(), 2);
QCOMPARE(evaluateJavaScriptSync(&page, "document.images.length").toInt(), 1);
QCOMPARE(evaluateJavaScriptSync(&page, "document.images[0].width").toInt(), 128);
QCOMPARE(evaluateJavaScriptSync(&page, "document.images[0].height").toInt(), 128);
}
void tst_QWebEnginePage::setHtmlWithStylesheetResource()
{
const char* htmlData =
"<html>"
"<head>"
"<link rel='stylesheet' href='qrc:/resources/style.css' type='text/css' />"
"</head>"
"<body>"
"<p id='idP'>some text</p>"
"</body>"
"</html>";
QLatin1String html(htmlData);
QWebEnginePage page;
QSignalSpy spyFinished(&page, &QWebEnginePage::loadFinished);
// We allow access to qrc resources from any security origin, including local and anonymous
page.setHtml(html, QUrl("file:///path/to/file"));
QVERIFY(spyFinished.wait());
QCOMPARE(evaluateJavaScriptSync(&page, "window.getComputedStyle(document.getElementById('idP')).color").toString(), QString("rgb(255, 0, 0)"));
page.setHtml(html, QUrl(QLatin1String("qrc:/")));
QVERIFY(spyFinished.wait());
QCOMPARE(evaluateJavaScriptSync(&page, "window.getComputedStyle(document.getElementById('idP')).color").toString(), QString("rgb(255, 0, 0)"));
// Now we test the opposite: without a baseUrl as a local file, we can still request qrc resources.
page.setHtml(html);
QVERIFY(spyFinished.wait());
QCOMPARE(evaluateJavaScriptSync(&page, "window.getComputedStyle(document.getElementById('idP')).color").toString(), QString("rgb(255, 0, 0)"));
}
void tst_QWebEnginePage::setHtmlWithBaseURL()
{
// This tests if baseUrl is indeed affecting the relative paths from resources.
// As we are using a local file as baseUrl, its security origin should be able to load local resources.
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);
QString html("<html><body><p>hello world</p><img src='resources/image2.png'/></body></html>");
QWebEnginePage page;
// in few seconds, the image should be completey loaded
QSignalSpy spy(&page, SIGNAL(loadFinished(bool)));
page.setHtml(html, QUrl::fromLocalFile(TESTS_SOURCE_DIR));
QSignalSpy spyFinished(&page, &QWebEnginePage::loadFinished);
QVERIFY(spyFinished.wait());
QCOMPARE(spy.count(), 1);
QCOMPARE(evaluateJavaScriptSync(&page, "document.images.length").toInt(), 1);
QCOMPARE(evaluateJavaScriptSync(&page, "document.images[0].width").toInt(), 128);
QCOMPARE(evaluateJavaScriptSync(&page, "document.images[0].height").toInt(), 128);
// no history item has to be added.
QCOMPARE(m_view->page()->history()->count(), 0);
}
class MyPage : public QWebEnginePage
{
public:
MyPage() : QWebEnginePage(), alerts(0) {}
int alerts;
protected:
virtual void javaScriptAlert(const QUrl &securityOrigin, const QString &msg)
{
alerts++;
QCOMPARE(securityOrigin, QUrl(QStringLiteral("http://test.origin.com/")));
QCOMPARE(msg, QString("foo"));
}
};
void tst_QWebEnginePage::setHtmlWithJSAlert()
{
QString html("<html><head></head><body><script>alert('foo');</script><p>hello world</p></body></html>");
MyPage page;
page.setHtml(html, QUrl(QStringLiteral("http://test.origin.com/path#fragment")));
QSignalSpy spyFinished(&page, &QWebEnginePage::loadFinished);
QVERIFY(spyFinished.wait());
QCOMPARE(page.alerts, 1);
QCOMPARE(toHtmlSync(&page), html);
}
void tst_QWebEnginePage::setHtmlWithModuleImport()
{
HttpServer server;
connect(&server, &HttpServer::newRequest, [&](HttpReqRep *rr) {
if (rr->requestMethod() == "GET" && rr->requestPath() == "/fibonacci.mjs") {
rr->setResponseBody("export function fib(n) {\n"
" return n < 2 ? n : fib(n-1) + fib(n-2)\n"
"}\n");
rr->setResponseHeader("Content-Type", "text/javascript");
rr->sendResponse();
} else {
rr->setResponseStatus(404);
rr->sendResponse();
}
});
QVERIFY(server.start());
QString html("<html>\n"
" <head>\n"
" <script type='module'>\n"
" import {fib} from './fibonacci.mjs'\n"
" window.fib7 = fib(7)\n"
" </script>\n"
" </head>\n"
" <body></body>\n"
"</html>\n");
QWebEnginePage page;
QSignalSpy spy(&page, &QWebEnginePage::loadFinished);
page.setHtml(html, server.url());
QVERIFY(spy.count() || spy.wait());
QCOMPARE(evaluateJavaScriptSync(&page, "fib7"), QVariant(13));
}
void tst_QWebEnginePage::baseUrl_data()
{
QTest::addColumn<QString>("html");
QTest::addColumn<QUrl>("loadUrl");
QTest::addColumn<QUrl>("url");
QTest::addColumn<QUrl>("baseUrl");
QTest::newRow("null") << QString() << QUrl()
<< QUrl("about:blank") << QUrl("about:blank");
QTest::newRow("foo") << QString() << QUrl("http://foobar.baz/")
<< QUrl("http://foobar.baz/") << QUrl("http://foobar.baz/");
QString html = "<html>"
"<head>"
"<base href=\"http://foobaz.bar/\" />"
"</head>"
"</html>";
QTest::newRow("customBaseUrl") << html << QUrl("http://foobar.baz/")
<< QUrl("http://foobar.baz/") << QUrl("http://foobaz.bar/");
}
void tst_QWebEnginePage::baseUrl()
{
QFETCH(QString, html);
QFETCH(QUrl, loadUrl);
QFETCH(QUrl, url);
QFETCH(QUrl, baseUrl);
QSignalSpy loadSpy(m_page, SIGNAL(loadFinished(bool)));
m_page->setHtml(html, loadUrl);
QTRY_COMPARE(loadSpy.count(), 1);
QCOMPARE(m_page->url(), url);
QEXPECT_FAIL("null", "Slight change: We now translate QUrl() to about:blank for the virtual url, but not for the baseUrl", Continue);
QCOMPARE(baseUrlSync(m_page), baseUrl);
}
void tst_QWebEnginePage::scrollPosition()
{
// enlarged image in a small viewport, to provoke the scrollbars to appear
QString html("<html><body><img src='qrc:/image.png' height=500 width=500/></body></html>");
QWebEngineView view;
view.setFixedSize(200,200);
view.show();
QVERIFY(QTest::qWaitForWindowExposed(&view));
QSignalSpy loadSpy(view.page(), SIGNAL(loadFinished(bool)));
view.setHtml(html);
QTRY_COMPARE(loadSpy.count(), 1);
// try to set the scroll offset programmatically
view.page()->runJavaScript("window.scrollTo(23, 29);");
QTRY_COMPARE(view.page()->scrollPosition().x(), 23 * view.windowHandle()->devicePixelRatio());
QCOMPARE(view.page()->scrollPosition().y(), 29 * view.windowHandle()->devicePixelRatio());
int x = evaluateJavaScriptSync(view.page(), "window.scrollX").toInt();
int y = evaluateJavaScriptSync(view.page(), "window.scrollY").toInt();
QCOMPARE(x, 23);
QCOMPARE(y, 29);
}
void tst_QWebEnginePage::scrollbarsOff()
{
QWebEngineView view;
view.page()->settings()->setAttribute(QWebEngineSettings::ShowScrollBars, false);
QString html("<html><body>"
" <div style='margin-top:1000px ; margin-left:1000px'>"
" <a id='offscreen' href='a'>End</a>"
" </div>"
"</body></html>");
QSignalSpy loadSpy(&view, SIGNAL(loadFinished(bool)));
view.setHtml(html);
QTRY_COMPARE(loadSpy.count(), 1);
QVERIFY(evaluateJavaScriptSync(view.page(), "innerWidth == document.documentElement.offsetWidth").toBool());
}
class WebView : public QWebEngineView
{
Q_OBJECT
signals:
void repaintRequested();
protected:
bool event(QEvent *event) {
if (event->type() == QEvent::UpdateRequest)
emit repaintRequested();
return QWebEngineView::event(event);
}
};
void tst_QWebEnginePage::evaluateWillCauseRepaint()
{
WebView view;
view.resize(640, 480);
view.show();
QVERIFY(QTest::qWaitForWindowExposed(&view));
QString html("<html><body>"
" top"
" <div id=\"junk\" style=\"display: block;\">junk</div>"
" bottom"
"</body></html>");
QSignalSpy loadSpy(&view, SIGNAL(loadFinished(bool)));
view.setHtml(html);
QTRY_COMPARE(loadSpy.count(), 1);
evaluateJavaScriptSync(view.page(), "document.getElementById('junk').style.display = 'none';");
QSignalSpy repaintSpy(&view, &WebView::repaintRequested);
QVERIFY(repaintSpy.wait());
}
void tst_QWebEnginePage::setContent_data()
{
QTest::addColumn<QString>("mimeType");
QTest::addColumn<QByteArray>("testContents");
QTest::addColumn<QString>("expected");
QString str = QString::fromUtf8("ὕαλον ϕαγεῖν δύναμαι· τοῦτο οὔ με βλάπτει");
QTest::newRow("UTF-8 plain text") << "text/plain; charset=utf-8" << str.toUtf8() << str;
QTextCodec *utf16 = QTextCodec::codecForName("UTF-16");
if (utf16)
QTest::newRow("UTF-16 plain text") << "text/plain; charset=utf-16" << utf16->fromUnicode(str) << str;
str = QString::fromUtf8("Une chaîne de caractères à sa façon.");
QTest::newRow("latin-1 plain text") << "text/plain; charset=iso-8859-1" << str.toLatin1() << str;
}
void tst_QWebEnginePage::setContent()
{
QFETCH(QString, mimeType);
QFETCH(QByteArray, testContents);
QFETCH(QString, expected);
QSignalSpy loadSpy(m_page, SIGNAL(loadFinished(bool)));
m_view->setContent(testContents, mimeType);
QVERIFY(loadSpy.wait());
QCOMPARE(toPlainTextSync(m_view->page()), expected);
}
class CacheNetworkAccessManager : public QNetworkAccessManager {
public:
CacheNetworkAccessManager(QObject* parent = 0)
: QNetworkAccessManager(parent)
, m_lastCacheLoad(QNetworkRequest::PreferNetwork)
{
}
virtual QNetworkReply* createRequest(Operation, const QNetworkRequest& request, QIODevice*)
{
QVariant cacheLoad = request.attribute(QNetworkRequest::CacheLoadControlAttribute);
if (cacheLoad.isValid())
m_lastCacheLoad = static_cast<QNetworkRequest::CacheLoadControl>(cacheLoad.toUInt());
else
m_lastCacheLoad = QNetworkRequest::PreferNetwork; // default value
return new FakeReply(request, this);
}
QNetworkRequest::CacheLoadControl lastCacheLoad() const
{
return m_lastCacheLoad;
}
private:
QNetworkRequest::CacheLoadControl m_lastCacheLoad;
};
void tst_QWebEnginePage::setUrlWithPendingLoads()
{
QWebEnginePage page;
page.setHtml("<img src='dummy:'/>");
page.setUrl(QUrl("about:blank"));
}
void tst_QWebEnginePage::setUrlToEmpty()
{
int expectedLoadFinishedCount = 0;
const QUrl aboutBlank("about:blank");
const QUrl url("qrc:/resources/test2.html");
QWebEnginePage page;
QCOMPARE(page.url(), QUrl());
QCOMPARE(page.requestedUrl(), QUrl());
// Chromium now returns about:blank as the base url here:
// QCOMPARE(baseUrlSync(&page), QUrl());
QSignalSpy spy(&page, SIGNAL(loadFinished(bool)));
// Set existing url
page.setUrl(url);
expectedLoadFinishedCount++;
QVERIFY(spy.wait());
QCOMPARE(spy.count(), expectedLoadFinishedCount);
QCOMPARE(page.url(), url);
QCOMPARE(page.requestedUrl(), url);
QCOMPARE(baseUrlSync(&page), url);
// Set empty url
page.setUrl(QUrl());
expectedLoadFinishedCount++;
QTRY_COMPARE(spy.count(), expectedLoadFinishedCount);
QCOMPARE(page.url(), aboutBlank);
QCOMPARE(page.requestedUrl(), QUrl());
QCOMPARE(baseUrlSync(&page), aboutBlank);
// Set existing url
page.setUrl(url);
expectedLoadFinishedCount++;
QTRY_COMPARE(spy.count(), expectedLoadFinishedCount);
QCOMPARE(page.url(), url);
QCOMPARE(page.requestedUrl(), url);
QCOMPARE(baseUrlSync(&page), url);
// Load empty url
page.load(QUrl());
expectedLoadFinishedCount++;
QTRY_COMPARE(spy.count(), expectedLoadFinishedCount);
QCOMPARE(page.url(), aboutBlank);
QCOMPARE(page.requestedUrl(), QUrl());
QCOMPARE(baseUrlSync(&page), aboutBlank);
}
void tst_QWebEnginePage::setUrlToInvalid()
{
QEXPECT_FAIL("", "Unsupported: QtWebEngine doesn't adjust invalid URLs.", Abort);
QVERIFY(false);
QWebEnginePage page;
const QUrl invalidUrl("http:/example.com");
QVERIFY(!invalidUrl.isEmpty());
QVERIFY(invalidUrl != QUrl());
// QWebEnginePage will do its best to accept the URL, possible converting it to a valid equivalent URL.
const QUrl validUrl("http://example.com/");
page.setUrl(invalidUrl);
QCOMPARE(page.url(), validUrl);
QCOMPARE(page.requestedUrl(), validUrl);
QCOMPARE(baseUrlSync(&page), validUrl);
// QUrls equivalent to QUrl() will be treated as such.
const QUrl aboutBlank("about:blank");
const QUrl anotherInvalidUrl("1http://bugs.webkit.org");
QVERIFY(!anotherInvalidUrl.isEmpty()); // and they are not necessarily empty.
QVERIFY(!anotherInvalidUrl.isValid());
QCOMPARE(anotherInvalidUrl.toEncoded(), QUrl().toEncoded());
page.setUrl(anotherInvalidUrl);
QCOMPARE(page.url(), aboutBlank);
QCOMPARE(page.requestedUrl().toEncoded(), anotherInvalidUrl.toEncoded());
QCOMPARE(baseUrlSync(&page), aboutBlank);
}
void tst_QWebEnginePage::setUrlToBadDomain()
{
// Failing to load a URL should still emit a urlChanged signal.
//
// This test is based on the scenario in QTBUG-48995 where the second setUrl
// call first triggers an unexpected additional urlChanged signal with the
// original url before the expected signal with the new url.
// RFC 2606 says the .invalid TLD should be invalid.
const QUrl url1 = QStringLiteral("http://this.is.definitely.invalid/");
const QUrl url2 = QStringLiteral("http://this.is.also.invalid/");
QWebEnginePage page;
QSignalSpy urlSpy(&page, &QWebEnginePage::urlChanged);
QSignalSpy titleSpy(&page, &QWebEnginePage::titleChanged);
QSignalSpy loadSpy(&page, &QWebEnginePage::loadFinished);
page.setUrl(url1);
QTRY_COMPARE(urlSpy.count(), 1);
QTRY_COMPARE_WITH_TIMEOUT(titleSpy.count(), 1, 20000);
QTRY_COMPARE(loadSpy.count(), 1);
QCOMPARE(urlSpy.takeFirst().value(0).toUrl(), url1);
QCOMPARE(titleSpy.takeFirst().value(0).toString(), url1.host());
QCOMPARE(loadSpy.takeFirst().value(0).toBool(), false);
QCOMPARE(page.url(), url1);
QCOMPARE(page.title(), url1.host());
page.setUrl(url2);
QTRY_COMPARE(urlSpy.count(), 1);
QTRY_COMPARE_WITH_TIMEOUT(titleSpy.count(), 1, 20000);
QTRY_COMPARE(loadSpy.count(), 1);
QCOMPARE(urlSpy.takeFirst().value(0).toUrl(), url2);
QCOMPARE(titleSpy.takeFirst().value(0).toString(), url2.host());
QCOMPARE(loadSpy.takeFirst().value(0).toBool(), false);
QCOMPARE(page.url(), url2);
QCOMPARE(page.title(), url2.host());
}
void tst_QWebEnginePage::setUrlToBadPort()
{
// Failing to load a URL should still emit a urlChanged signal.
// Ports 244-245 are hopefully unbound (marked unassigned in RFC1700).
const QUrl url1 = QStringLiteral("http://127.0.0.1:244/");
const QUrl url2 = QStringLiteral("http://127.0.0.1:245/");
QWebEnginePage page;
QSignalSpy urlSpy(&page, &QWebEnginePage::urlChanged);
QSignalSpy titleSpy(&page, &QWebEnginePage::titleChanged);
QSignalSpy loadSpy(&page, &QWebEnginePage::loadFinished);
page.setUrl(url1);
QTRY_COMPARE(urlSpy.count(), 1);
QTRY_COMPARE(titleSpy.count(), 2);
QTRY_COMPARE(loadSpy.count(), 1);
QCOMPARE(urlSpy.takeFirst().value(0).toUrl(), url1);
QCOMPARE(titleSpy.takeFirst().value(0).toString(), url1.authority());
QCOMPARE(titleSpy.takeFirst().value(0).toString(), url1.host());
QCOMPARE(loadSpy.takeFirst().value(0).toBool(), false);
QCOMPARE(page.url(), url1);
QCOMPARE(page.title(), url1.host());
page.setUrl(url2);
QTRY_COMPARE(urlSpy.count(), 1);
QTRY_COMPARE(titleSpy.count(), 2);
QTRY_COMPARE(loadSpy.count(), 1);
QCOMPARE(urlSpy.takeFirst().value(0).toUrl(), url2);
QCOMPARE(titleSpy.takeFirst().value(0).toString(), url2.authority());
QCOMPARE(titleSpy.takeFirst().value(0).toString(), url2.host());
QCOMPARE(loadSpy.takeFirst().value(0).toBool(), false);
QCOMPARE(page.url(), url2);
QCOMPARE(page.title(), url2.host());
}
static QStringList collectHistoryUrls(QWebEngineHistory *history)
{
QStringList urls;
const QList<QWebEngineHistoryItem> items = history->items();
for (const QWebEngineHistoryItem &i : items)
urls << i.url().toString();
return urls;
}
void tst_QWebEnginePage::setUrlHistory()
{
const QUrl aboutBlank("about:blank");
QUrl url;
int expectedLoadFinishedCount = 0;
QSignalSpy spy(m_page, SIGNAL(loadFinished(bool)));
QCOMPARE(m_page->history()->count(), 0);
m_page->setUrl(QUrl());
expectedLoadFinishedCount++;
QTRY_COMPARE(spy.count(), expectedLoadFinishedCount);
QCOMPARE(m_page->url(), aboutBlank);
QCOMPARE(m_page->requestedUrl(), QUrl());
// Chromium stores navigation entry for every successful loads. The load of the empty page is committed and stored as about:blank.
QCOMPARE(collectHistoryUrls(m_page->history()), QStringList() << aboutBlank.toString());
url = QUrl("http://url.invalid/");
m_page->setUrl(url);
expectedLoadFinishedCount++;
QTRY_COMPARE_WITH_TIMEOUT(spy.count(), expectedLoadFinishedCount, 20000);
// When error page is disabled in case of LoadFail the entry of the unavailable page is not stored.
// We expect the url of the previously loaded page here.
QCOMPARE(m_page->url(), aboutBlank);
QCOMPARE(m_page->requestedUrl(), QUrl());
// Since the entry of the unavailable page is not stored it will not available in the history.
QCOMPARE(collectHistoryUrls(m_page->history()), QStringList() << aboutBlank.toString());
url = QUrl("qrc:/resources/test1.html");
m_page->setUrl(url);
expectedLoadFinishedCount++;
QTRY_COMPARE(spy.count(), expectedLoadFinishedCount);
QCOMPARE(m_page->url(), url);
QCOMPARE(m_page->requestedUrl(), url);
QCOMPARE(collectHistoryUrls(m_page->history()), QStringList() << aboutBlank.toString() << QStringLiteral("qrc:/resources/test1.html"));
m_page->setUrl(QUrl());
expectedLoadFinishedCount++;
QTRY_COMPARE(spy.count(), expectedLoadFinishedCount);
QCOMPARE(m_page->url(), aboutBlank);
QCOMPARE(m_page->requestedUrl(), QUrl());
// Chromium stores navigation entry for every successful loads. The load of the empty page is committed and stored as about:blank.
QCOMPARE(collectHistoryUrls(m_page->history()), QStringList()
<< aboutBlank.toString()
<< QStringLiteral("qrc:/resources/test1.html")
<< aboutBlank.toString());
url = QUrl("qrc:/resources/test1.html");
m_page->setUrl(url);
expectedLoadFinishedCount++;
QTRY_COMPARE(spy.count(), expectedLoadFinishedCount);
QCOMPARE(m_page->url(), url);
QCOMPARE(m_page->requestedUrl(), url);
// The history count DOES change since the about:blank is in the list.
QCOMPARE(collectHistoryUrls(m_page->history()), QStringList()
<< aboutBlank.toString()
<< QStringLiteral("qrc:/resources/test1.html")
<< aboutBlank.toString()
<< QStringLiteral("qrc:/resources/test1.html"));
url = QUrl("qrc:/resources/test2.html");
m_page->setUrl(url);
expectedLoadFinishedCount++;
QTRY_COMPARE(spy.count(), expectedLoadFinishedCount);
QCOMPARE(m_page->url(), url);
QCOMPARE(m_page->requestedUrl(), url);
QCOMPARE(collectHistoryUrls(m_page->history()), QStringList()
<< aboutBlank.toString()
<< QStringLiteral("qrc:/resources/test1.html")
<< aboutBlank.toString()
<< QStringLiteral("qrc:/resources/test1.html")
<< QStringLiteral("qrc:/resources/test2.html"));
}
void tst_QWebEnginePage::setUrlUsingStateObject()
{
QUrl url;
QSignalSpy urlChangedSpy(m_page, SIGNAL(urlChanged(QUrl)));
int expectedUrlChangeCount = 0;
QCOMPARE(m_page->history()->count(), 0);
url = QUrl("qrc:/resources/test1.html");
m_page->setUrl(url);
expectedUrlChangeCount++;
QTRY_COMPARE(urlChangedSpy.count(), expectedUrlChangeCount);
QCOMPARE(m_page->url(), url);
QTRY_COMPARE(m_page->history()->count(), 1);
evaluateJavaScriptSync(m_page, "window.history.pushState(null, 'push', 'navigate/to/here')");
expectedUrlChangeCount++;
QTRY_COMPARE(urlChangedSpy.count(), expectedUrlChangeCount);
QCOMPARE(m_page->url(), QUrl("qrc:/resources/navigate/to/here"));
QCOMPARE(m_page->history()->count(), 2);
QVERIFY(m_page->history()->canGoBack());
evaluateJavaScriptSync(m_page, "window.history.replaceState(null, 'replace', 'another/location')");
expectedUrlChangeCount++;
QTRY_COMPARE(urlChangedSpy.count(), expectedUrlChangeCount);
QCOMPARE(m_page->url(), QUrl("qrc:/resources/navigate/to/another/location"));
QCOMPARE(m_page->history()->count(), 2);
QVERIFY(!m_page->history()->canGoForward());
QVERIFY(m_page->history()->canGoBack());
evaluateJavaScriptSync(m_page, "window.history.back()");
expectedUrlChangeCount++;
QTRY_COMPARE(urlChangedSpy.count(), expectedUrlChangeCount);
QCOMPARE(m_page->url(), QUrl("qrc:/resources/test1.html"));
QVERIFY(m_page->history()->canGoForward());
QVERIFY(!m_page->history()->canGoBack());
}
static inline QUrl extractBaseUrl(const QUrl& url)
{
return url.resolved(QUrl());
}
void tst_QWebEnginePage::setUrlThenLoads_data()
{
QTest::addColumn<QUrl>("url");
QTest::addColumn<QUrl>("baseUrl");
QTest::newRow("resource file") << QUrl("qrc:/resources/test1.html") << extractBaseUrl(QUrl("qrc:/resources/test1.html"));
QTest::newRow("base specified in HTML") << QUrl("data:text/html,<head><base href=\"http://different.base/\"></head>") << QUrl("http://different.base/");
}
void tst_QWebEnginePage::setUrlThenLoads()
{
QFETCH(QUrl, url);
QFETCH(QUrl, baseUrl);
QSignalSpy urlChangedSpy(m_page, SIGNAL(urlChanged(QUrl)));
QSignalSpy startedSpy(m_page, SIGNAL(loadStarted()));
QSignalSpy finishedSpy(m_page, SIGNAL(loadFinished(bool)));
m_page->setUrl(url);
QTRY_COMPARE(startedSpy.count(), 1);
QTRY_COMPARE(urlChangedSpy.count(), 1);
QTRY_COMPARE(finishedSpy.count(), 1);
QVERIFY(finishedSpy.at(0).first().toBool());
QCOMPARE(m_page->url(), url);
QCOMPARE(m_page->requestedUrl(), url);
QCOMPARE(baseUrlSync(m_page), baseUrl);
const QUrl urlToLoad1("qrc:/resources/test2.html");
const QUrl urlToLoad2("qrc:/resources/test1.html");
m_page->load(urlToLoad1);
QTRY_COMPARE(m_page->url(), urlToLoad1);
QTRY_COMPARE(m_page->requestedUrl(), urlToLoad1);
// baseUrlSync spins an event loop and this sometimes return the next result.
// QCOMPARE(baseUrlSync(m_page), baseUrl);
QTRY_COMPARE(startedSpy.count(), 2);
// After first URL changed.
QTRY_COMPARE(urlChangedSpy.count(), 2);
QTRY_COMPARE(finishedSpy.count(), 2);
QVERIFY(finishedSpy.at(1).first().toBool());
QCOMPARE(m_page->url(), urlToLoad1);
QCOMPARE(m_page->requestedUrl(), urlToLoad1);
QCOMPARE(baseUrlSync(m_page), extractBaseUrl(urlToLoad1));
m_page->load(urlToLoad2);
QCOMPARE(m_page->url(), urlToLoad1);
QCOMPARE(m_page->requestedUrl(), urlToLoad2);
QCOMPARE(baseUrlSync(m_page), extractBaseUrl(urlToLoad1));
QTRY_COMPARE(startedSpy.count(), 3);
// After second URL changed.
QTRY_COMPARE(urlChangedSpy.count(), 3);
QTRY_COMPARE(finishedSpy.count(), 3);
QVERIFY(finishedSpy.at(2).first().toBool());
QCOMPARE(m_page->url(), urlToLoad2);
QCOMPARE(m_page->requestedUrl(), urlToLoad2);
QCOMPARE(baseUrlSync(m_page), extractBaseUrl(urlToLoad2));
}
void tst_QWebEnginePage::loadFinishedAfterNotFoundError()
{
QWebEnginePage page;
QSignalSpy spy(&page, SIGNAL(loadFinished(bool)));
page.settings()->setAttribute(QWebEngineSettings::ErrorPageEnabled, false);
page.setUrl(QUrl("http://non.existent/url"));
QTRY_COMPARE_WITH_TIMEOUT(spy.count(), 1, 20000);
page.settings()->setAttribute(QWebEngineSettings::ErrorPageEnabled, true);
page.setUrl(QUrl("http://another.non.existent/url"));
QTRY_COMPARE_WITH_TIMEOUT(spy.count(), 2, 20000);
}
class URLSetter : public QObject {
Q_OBJECT
public:
enum Signal {
LoadStarted,
LoadFinished,
};
enum Type {
UseLoad,
UseSetUrl
};
URLSetter(QWebEnginePage*, Signal, Type, const QUrl&);
public Q_SLOTS:
void execute();
Q_SIGNALS:
void finished();
private:
QWebEnginePage* m_page;
QUrl m_url;
Type m_type;
};
Q_DECLARE_METATYPE(URLSetter::Signal)
Q_DECLARE_METATYPE(URLSetter::Type)
URLSetter::URLSetter(QWebEnginePage* page, Signal signal, URLSetter::Type type, const QUrl& url)
: m_page(page), m_url(url), m_type(type)
{
if (signal == LoadStarted)
connect(m_page, SIGNAL(loadStarted()), SLOT(execute()));
else if (signal == LoadFinished)
connect(m_page, SIGNAL(loadFinished(bool)), SLOT(execute()));
}
void URLSetter::execute()
{
// We track only the first emission.
m_page->disconnect(this);
connect(m_page, SIGNAL(loadFinished(bool)), SIGNAL(finished()));
if (m_type == URLSetter::UseLoad)
m_page->load(m_url);
else
m_page->setUrl(m_url);
}
void tst_QWebEnginePage::loadInSignalHandlers_data()
{
QTest::addColumn<URLSetter::Type>("type");
QTest::addColumn<URLSetter::Signal>("signal");
QTest::addColumn<QUrl>("url");
const QUrl validUrl("qrc:/resources/test2.html");
const QUrl invalidUrl("qrc:/invalid");
QTest::newRow("call load() in loadStarted() after valid url") << URLSetter::UseLoad << URLSetter::LoadStarted << validUrl;
QTest::newRow("call load() in loadStarted() after invalid url") << URLSetter::UseLoad << URLSetter::LoadStarted << invalidUrl;
QTest::newRow("call load() in loadFinished() after valid url") << URLSetter::UseLoad << URLSetter::LoadFinished << validUrl;
QTest::newRow("call load() in loadFinished() after invalid url") << URLSetter::UseLoad << URLSetter::LoadFinished << invalidUrl;
QTest::newRow("call setUrl() in loadStarted() after valid url") << URLSetter::UseSetUrl << URLSetter::LoadStarted << validUrl;
QTest::newRow("call setUrl() in loadStarted() after invalid url") << URLSetter::UseSetUrl << URLSetter::LoadStarted << invalidUrl;
QTest::newRow("call setUrl() in loadFinished() after valid url") << URLSetter::UseSetUrl << URLSetter::LoadFinished << validUrl;
QTest::newRow("call setUrl() in loadFinished() after invalid url") << URLSetter::UseSetUrl << URLSetter::LoadFinished << invalidUrl;
}
void tst_QWebEnginePage::loadInSignalHandlers()
{
QFETCH(URLSetter::Type, type);
QFETCH(URLSetter::Signal, signal);
QFETCH(QUrl, url);
const QUrl urlForSetter("qrc:/resources/test1.html");
URLSetter setter(m_page, signal, type, urlForSetter);
QSignalSpy spy(&setter, &URLSetter::finished);
m_page->load(url);
// every loadStarted() call should have also loadFinished()
if (signal == URLSetter::LoadStarted)
QTRY_COMPARE(spy.count(), 2);
else
QTRY_COMPARE(spy.count(), 1);
QCOMPARE(m_page->url(), urlForSetter);
}
void tst_QWebEnginePage::loadFromQrc()
{
QWebEnginePage page;
QSignalSpy spy(&page, &QWebEnginePage::loadFinished);
// Standard case.
page.load(QStringLiteral("qrc:///resources/foo.txt"));
QTRY_COMPARE(spy.count(), 1);
QCOMPARE(spy.takeFirst().value(0).toBool(), true);
QCOMPARE(toPlainTextSync(&page), QStringLiteral("foo\n"));
// Query and fragment parts are ignored.
page.load(QStringLiteral("qrc:///resources/bar.txt?foo=1#bar"));
QTRY_COMPARE(spy.count(), 1);
QCOMPARE(spy.takeFirst().value(0).toBool(), true);
QCOMPARE(toPlainTextSync(&page), QStringLiteral("bar\n"));
// Literal spaces are OK.
page.load(QStringLiteral("qrc:///resources/path with spaces.txt"));
QTRY_COMPARE(spy.count(), 1);
QCOMPARE(spy.takeFirst().value(0).toBool(), true);
QCOMPARE(toPlainTextSync(&page), QStringLiteral("contents with spaces\n"));
// Escaped spaces are OK too.
page.load(QStringLiteral("qrc:///resources/path%20with%20spaces.txt"));
QTRY_COMPARE(spy.count(), 1);
QCOMPARE(spy.takeFirst().value(0).toBool(), true);
QCOMPARE(toPlainTextSync(&page), QStringLiteral("contents with spaces\n"));
// Resource not found, loading fails.
page.load(QStringLiteral("qrc:///nope"));
QTRY_COMPARE(spy.count(), 1);
QCOMPARE(spy.takeFirst().value(0).toBool(), false);
}
#if QT_CONFIG(webengine_webchannel)
void tst_QWebEnginePage::restoreHistory()
{
QWebChannel channel;
QWebEnginePage page;
page.setWebChannel(&channel);
QWebEngineScript script;
script.setName(QStringLiteral("script"));
page.scripts().insert(script);
QSignalSpy spy(&page, SIGNAL(loadFinished(bool)));
page.load(QUrl(QStringLiteral("qrc:/resources/test1.html")));
QTRY_COMPARE(spy.count(), 1);
QCOMPARE(page.webChannel(), &channel);
QVERIFY(page.scripts().contains(script));
QByteArray data;
QDataStream out(&data, QIODevice::ReadWrite);
out << *page.history();
QDataStream in(&data, QIODevice::ReadOnly);
in >> *page.history();
QTRY_COMPARE(spy.count(), 2);
QCOMPARE(page.webChannel(), &channel);
QVERIFY(page.scripts().contains(script));
}
#endif
void tst_QWebEnginePage::toPlainTextLoadFinishedRace_data()
{
QTest::addColumn<bool>("enableErrorPage");
QTest::newRow("disableErrorPage") << false;
QTest::newRow("enableErrorPage") << true;
}
void tst_QWebEnginePage::toPlainTextLoadFinishedRace()
{
QFETCH(bool, enableErrorPage);
QScopedPointer<QWebEnginePage> page(new QWebEnginePage);
page->settings()->setAttribute(QWebEngineSettings::ErrorPageEnabled, enableErrorPage);
QSignalSpy spy(page.data(), SIGNAL(loadFinished(bool)));
page->load(QUrl("data:text/plain,foobarbaz"));
QTRY_VERIFY(spy.count() == 1);
QCOMPARE(toPlainTextSync(page.data()), QString("foobarbaz"));
page->load(QUrl("http://fail.invalid/"));
QTRY_COMPARE_WITH_TIMEOUT(spy.count(), 2, 20000);
QString s = toPlainTextSync(page.data());
QVERIFY(s.contains("foobarbaz") == !enableErrorPage);
page->load(QUrl("data:text/plain,lalala"));
QTRY_COMPARE(spy.count(), 3);
QTRY_COMPARE(toPlainTextSync(page.data()), QString("lalala"));
page.reset();
QCOMPARE(spy.count(), 3);
}
void tst_QWebEnginePage::setZoomFactor()
{
QWebEnginePage page;
QVERIFY(qFuzzyCompare(page.zoomFactor(), 1.0));
page.setZoomFactor(2.5);
QVERIFY(qFuzzyCompare(page.zoomFactor(), 2.5));
const QUrl urlToLoad("qrc:/resources/test1.html");
QSignalSpy finishedSpy(&page, SIGNAL(loadFinished(bool)));
page.load(urlToLoad);
QTRY_COMPARE(finishedSpy.count(), 1);
QVERIFY(finishedSpy.at(0).first().toBool());
QVERIFY(qFuzzyCompare(page.zoomFactor(), 2.5));
page.setZoomFactor(5.5);
QVERIFY(qFuzzyCompare(page.zoomFactor(), 2.5));
page.setZoomFactor(0.1);
QVERIFY(qFuzzyCompare(page.zoomFactor(), 2.5));
}
void tst_QWebEnginePage::mouseButtonTranslation()
{
QWebEngineView view;
QSignalSpy spy(&view, SIGNAL(loadFinished(bool)));
view.setHtml(QStringLiteral(
"<html><head><script>\
var lastEvent = { 'button' : -1 }; \
function saveLastEvent(event) { console.log(event); lastEvent = event; }; \
</script></head>\
<body>\
<div style=\"height:600px;\" onmousedown=\"saveLastEvent(event)\">\
</div>\
</body></html>"));
view.resize(640, 480);
view.show();
QVERIFY(QTest::qWaitForWindowExposed(&view));
QTRY_VERIFY(spy.count() == 1);
QVERIFY(view.focusProxy() != nullptr);
QMouseEvent evpres(QEvent::MouseButtonPress, view.rect().center(), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier);
QGuiApplication::sendEvent(view.focusProxy(), &evpres);
QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "lastEvent.button").toInt(), 0);
QCOMPARE(evaluateJavaScriptSync(view.page(), "lastEvent.buttons").toInt(), 1);
QMouseEvent evpres2(QEvent::MouseButtonPress, view.rect().center(), Qt::RightButton, Qt::LeftButton | Qt::RightButton, Qt::NoModifier);
QGuiApplication::sendEvent(view.focusProxy(), &evpres2);
QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "lastEvent.button").toInt(), 2);
QCOMPARE(evaluateJavaScriptSync(view.page(), "lastEvent.buttons").toInt(), 3);
}
void tst_QWebEnginePage::mouseMovementProperties()
{
QWebEngineView view;
ConsolePage page;
view.setPage(&page);
view.resize(640, 480);
QTest::mouseMove(&view, QPoint(10, 10));
view.show();
QVERIFY(QTest::qWaitForWindowExposed(&view));
QSignalSpy loadFinishedSpy(&page, SIGNAL(loadFinished(bool)));
page.setHtml(QStringLiteral(
"<html><head><script>\
function onMouseMove(event) { console.log(event.movementX + \", \" + event.movementY); }; \
</script></head>\
<body>\
<div style=\"height:600px;\" onmousemove=\"onMouseMove(event)\">\
</div>\
</body></html>"));
loadFinishedSpy.wait();
QTest::mouseMove(&view, QPoint(20, 20));
QTRY_COMPARE(page.messages.count(), 1);
QTest::mouseMove(&view, QPoint(30, 30));
QTRY_COMPARE(page.messages.count(), 2);
QTRY_COMPARE(page.messages[1], QString("10, 10"));
QTest::mouseMove(&view, QPoint(20, 20));
QTRY_COMPARE(page.messages.count(), 3);
QTRY_COMPARE(page.messages[2], QString("-10, -10"));
}
QPoint tst_QWebEnginePage::elementCenter(QWebEnginePage *page, const QString &id)
{
QVariantList rectList = evaluateJavaScriptSync(page,
"(function(){"
"var elem = document.getElementById('" + id + "');"
"var rect = elem.getBoundingClientRect();"
"return [(rect.left + rect.right) / 2, (rect.top + rect.bottom) / 2];"
"})()").toList();
if (rectList.count() != 2) {
qWarning("elementCenter failed.");
return QPoint();
}
return QPoint(rectList.at(0).toInt(), rectList.at(1).toInt());
}
void tst_QWebEnginePage::viewSource()
{
TestPage page;
QSignalSpy loadFinishedSpy(&page, SIGNAL(loadFinished(bool)));
QSignalSpy windowCreatedSpy(&page, SIGNAL(windowCreated()));
const QUrl url("qrc:/resources/test1.html");
page.load(url);
QTRY_COMPARE(loadFinishedSpy.count(), 1);
QCOMPARE(page.title(), QStringLiteral("Test page 1"));
QVERIFY(page.action(QWebEnginePage::ViewSource)->isEnabled());
page.triggerAction(QWebEnginePage::ViewSource);
QTRY_COMPARE(windowCreatedSpy.count(), 1);
QCOMPARE(page.createdWindows.size(), 1);
QTRY_COMPARE(page.createdWindows[0]->url().toString(), QStringLiteral("view-source:%1").arg(url.toString()));
// The requested URL should not be about:blank if the qrc scheme is supported
QTRY_COMPARE(page.createdWindows[0]->requestedUrl(), url);
QTRY_COMPARE(page.createdWindows[0]->title(), QStringLiteral("view-source:%1").arg(url.toString()));
QVERIFY(!page.createdWindows[0]->action(QWebEnginePage::ViewSource)->isEnabled());
}
void tst_QWebEnginePage::viewSourceURL_data()
{
QTest::addColumn<QUrl>("userInputUrl");
QTest::addColumn<bool>("loadSucceed");
QTest::addColumn<QUrl>("url");
QTest::addColumn<QUrl>("requestedUrl");
QTest::addColumn<QString>("title");
QTest::newRow("view-source:") << QUrl("view-source:") << true << QUrl("view-source:") << QUrl("about:blank") << QString("view-source:");
QTest::newRow("view-source:about:blank") << QUrl("view-source:about:blank") << true << QUrl("view-source:about:blank") << QUrl("about:blank") << QString("view-source:about:blank");
QString localFilePath = QString("%1qwebenginepage/resources/test1.html").arg(TESTS_SOURCE_DIR);
QUrl testLocalUrl = QUrl(QString("view-source:%1").arg(QUrl::fromLocalFile(localFilePath).toString()));
QUrl testLocalUrlWithoutScheme = QUrl(QString("view-source:%1").arg(localFilePath));
QTest::newRow(testLocalUrl.toString().toStdString().c_str()) << testLocalUrl << true << testLocalUrl << QUrl::fromLocalFile(localFilePath) << QString("test1.html");
QTest::newRow(testLocalUrlWithoutScheme.toString().toStdString().c_str()) << testLocalUrlWithoutScheme << true << testLocalUrl << QUrl::fromLocalFile(localFilePath) << QString("test1.html");
QString resourcePath = QLatin1String("qrc:/resources/test1.html");
QUrl testResourceUrl = QUrl(QString("view-source:%1").arg(resourcePath));
QTest::newRow(testResourceUrl.toString().toStdString().c_str()) << testResourceUrl << true << testResourceUrl << QUrl(resourcePath) << testResourceUrl.toString();
QTest::newRow("view-source:http://non.existent") << QUrl("view-source:non.existent") << false << QUrl("view-source:http://non.existent/") << QUrl("http://non.existent/") << QString("non.existent");
QTest::newRow("view-source:non.existent") << QUrl("view-source:non.existent") << false << QUrl("view-source:http://non.existent/") << QUrl("http://non.existent/") << QString("non.existent");
}
void tst_QWebEnginePage::viewSourceURL()
{
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);
QFETCH(QUrl, userInputUrl);
QFETCH(bool, loadSucceed);
QFETCH(QUrl, url);
QFETCH(QUrl, requestedUrl);
QFETCH(QString, title);
QWebEnginePage page;
QSignalSpy loadFinishedSpy(&page, SIGNAL(loadFinished(bool)));
page.load(userInputUrl);
QTRY_COMPARE_WITH_TIMEOUT(loadFinishedSpy.count(), 1, 12000);
QList<QVariant> arguments = loadFinishedSpy.takeFirst();
QCOMPARE(arguments.at(0).toBool(), loadSucceed);
QCOMPARE(page.url(), url);
QCOMPARE(page.requestedUrl(), requestedUrl);
QCOMPARE(page.title(), title);
QVERIFY(!page.action(QWebEnginePage::ViewSource)->isEnabled());
}
void tst_QWebEnginePage::viewSourceCredentials()
{
TestPage page;
QSignalSpy loadFinishedSpy(&page, SIGNAL(loadFinished(bool)));
QSignalSpy windowCreatedSpy(&page, SIGNAL(windowCreated()));
QUrl url("http://user:passwd@httpbin.org/basic-auth/user/passwd");
// Test explicit view-source URL with credentials
page.load(QUrl(QString("view-source:" + url.toString())));
if (!loadFinishedSpy.wait(10000) || !loadFinishedSpy.at(0).at(0).toBool())
QSKIP("Couldn't load page from network, skipping test.");
QCOMPARE(page.url().toString(), QString("view-source:" + url.toDisplayString(QUrl::RemoveUserInfo)));
QCOMPARE(page.requestedUrl(), url);
QCOMPARE(page.title(), QString("view-source:" + url.toDisplayString(QUrl::RemoveScheme | QUrl::RemoveUserInfo).remove(0, 2)));
loadFinishedSpy.clear();
windowCreatedSpy.clear();
// Test ViewSource web action on URL with credentials
page.load(url);
if (!loadFinishedSpy.wait(10000) || !loadFinishedSpy.at(0).at(0).toBool())
QSKIP("Couldn't load page from network, skipping test.");
QVERIFY(page.action(QWebEnginePage::ViewSource)->isEnabled());
page.triggerAction(QWebEnginePage::ViewSource);
QTRY_COMPARE(windowCreatedSpy.count(), 1);
QCOMPARE(page.createdWindows.size(), 1);
QTRY_COMPARE(page.createdWindows[0]->url().toString(), QString("view-source:" + url.toDisplayString(QUrl::RemoveUserInfo)));
QTRY_COMPARE(page.createdWindows[0]->requestedUrl(), url);
QTRY_COMPARE(page.createdWindows[0]->title(), QString("view-source:" + url.toDisplayString(QUrl::RemoveScheme | QUrl::RemoveUserInfo).remove(0, 2)));
}
Q_DECLARE_METATYPE(QNetworkProxy::ProxyType);
void tst_QWebEnginePage::proxyConfigWithUnexpectedHostPortPair()
{
// Chromium expects a proxy of type NoProxy to not have a host or port set.
QNetworkProxy proxy;
proxy.setType(QNetworkProxy::NoProxy);
proxy.setHostName(QStringLiteral("127.0.0.1"));
proxy.setPort(244);
QNetworkProxy::setApplicationProxy(proxy);
QSignalSpy loadFinishedSpy(m_page, SIGNAL(loadFinished(bool)));
m_page->load(QStringLiteral("http://127.0.0.1:245/"));
QTRY_COMPARE(loadFinishedSpy.count(), 1);
}
void tst_QWebEnginePage::registerProtocolHandler_data()
{
QTest::addColumn<bool>("permission");
QTest::newRow("accept") << true;
QTest::newRow("reject") << false;
}
void tst_QWebEnginePage::registerProtocolHandler()
{
QFETCH(bool, permission);
HttpServer server;
int mailRequestCount = 0;
connect(&server, &HttpServer::newRequest, [&](HttpReqRep *rr) {
if (rr->requestMethod() == "GET" && rr->requestPath() == "/") {
rr->setResponseBody(QByteArrayLiteral("<html><body><a id=\"link\" href=\"mailto:foo@bar.com\">some text here</a></body></html>"));
rr->sendResponse();
} else if (rr->requestMethod() == "GET" && rr->requestPath() == "/mail?uri=mailto%3Afoo%40bar.com") {
mailRequestCount++;
rr->sendResponse();
} else {
rr->setResponseStatus(404);
rr->sendResponse();
}
});
QVERIFY(server.start());
QWebEnginePage page;
QSignalSpy loadSpy(&page, &QWebEnginePage::loadFinished);
QSignalSpy permissionSpy(&page, &QWebEnginePage::registerProtocolHandlerRequested);
page.setUrl(server.url("/"));
QTRY_COMPARE(loadSpy.count(), 1);
QCOMPARE(loadSpy.takeFirst().value(0).toBool(), true);
QString callFormat = QStringLiteral("window.navigator.registerProtocolHandler(\"%1\", \"%2\", \"%3\")");
QString scheme = QStringLiteral("mailto");
QString url = server.url("/mail").toString() + QStringLiteral("?uri=%s");
QString title;
QString call = callFormat.arg(scheme).arg(url).arg(title);
page.runJavaScript(call);
QTRY_COMPARE(permissionSpy.count(), 1);
auto request = permissionSpy.takeFirst().value(0).value<QWebEngineRegisterProtocolHandlerRequest>();
QCOMPARE(request.origin(), QUrl(url));
QCOMPARE(request.scheme(), scheme);
if (permission)
request.accept();
else
request.reject();
page.runJavaScript(QStringLiteral("document.getElementById(\"link\").click()"));
QTRY_COMPARE(loadSpy.count(), 1);
QCOMPARE(loadSpy.takeFirst().value(0).toBool(), permission);
QCOMPARE(mailRequestCount, permission ? 1 : 0);
QVERIFY(server.stop());
}
void tst_QWebEnginePage::dataURLFragment()
{
m_view->resize(800, 600);
m_view->show();
QSignalSpy loadFinishedSpy(m_page, SIGNAL(loadFinished(bool)));
m_page->setHtml("<html><body>"
"<a id='link' href='#anchor'>anchor</a>"
"</body></html>");
QTRY_COMPARE(loadFinishedSpy.count(), 1);
QSignalSpy urlChangedSpy(m_page, SIGNAL(urlChanged(QUrl)));
QTest::mouseClick(m_view->focusProxy(), Qt::LeftButton, {}, elementCenter(m_page, "link"));
QVERIFY(urlChangedSpy.wait());
QCOMPARE(m_page->url().fragment(), QStringLiteral("anchor"));
m_page->setHtml("<html><body>"
"<a id='link' href='#anchor'>anchor</a>"
"</body></html>", QUrl("http://test.qt.io/mytest.html"));
QTRY_COMPARE(loadFinishedSpy.count(), 2);
QTest::mouseClick(m_view->focusProxy(), Qt::LeftButton, {}, elementCenter(m_page, "link"));
QVERIFY(urlChangedSpy.wait());
QCOMPARE(m_page->url(), QUrl("http://test.qt.io/mytest.html#anchor"));
}
void tst_QWebEnginePage::devTools()
{
QWebEngineProfile profile;
QWebEnginePage inspectedPage1(&profile);
QWebEnginePage inspectedPage2(&profile);
QWebEnginePage devToolsPage(&profile);
QSignalSpy spy(&devToolsPage, &QWebEnginePage::loadFinished);
inspectedPage1.setDevToolsPage(&devToolsPage);
QCOMPARE(inspectedPage1.devToolsPage(), &devToolsPage);
QCOMPARE(inspectedPage1.inspectedPage(), nullptr);
QCOMPARE(inspectedPage2.devToolsPage(), nullptr);
QCOMPARE(inspectedPage2.inspectedPage(), nullptr);
QCOMPARE(devToolsPage.devToolsPage(), nullptr);
QCOMPARE(devToolsPage.inspectedPage(), &inspectedPage1);
QTRY_COMPARE_WITH_TIMEOUT(spy.count(), 1, 30000);
QVERIFY(spy.takeFirst().value(0).toBool());
devToolsPage.setInspectedPage(&inspectedPage2);
QCOMPARE(inspectedPage1.devToolsPage(), nullptr);
QCOMPARE(inspectedPage1.inspectedPage(), nullptr);
QCOMPARE(inspectedPage2.devToolsPage(), &devToolsPage);
QCOMPARE(inspectedPage2.inspectedPage(), nullptr);
QCOMPARE(devToolsPage.devToolsPage(), nullptr);
QCOMPARE(devToolsPage.inspectedPage(), &inspectedPage2);
QTRY_COMPARE_WITH_TIMEOUT(spy.count(), 1, 30000);
QVERIFY(spy.takeFirst().value(0).toBool());
devToolsPage.setInspectedPage(nullptr);
QCOMPARE(inspectedPage1.devToolsPage(), nullptr);
QCOMPARE(inspectedPage1.inspectedPage(), nullptr);
QCOMPARE(inspectedPage2.devToolsPage(), nullptr);
QCOMPARE(inspectedPage2.inspectedPage(), nullptr);
QCOMPARE(devToolsPage.devToolsPage(), nullptr);
QCOMPARE(devToolsPage.inspectedPage(), nullptr);
}
void tst_QWebEnginePage::openLinkInDifferentProfile()
{
class Page : public QWebEnginePage {
public:
QWebEnginePage *targetPage = nullptr;
Page(QWebEngineProfile *profile) : QWebEnginePage(profile) {}
private:
QWebEnginePage *createWindow(WebWindowType) override { return targetPage; }
};
QWebEngineProfile profile1, profile2;
Page page1(&profile1), page2(&profile2);
QWebEngineView view;
view.resize(500, 500);
view.setPage(&page1);
view.show();
QVERIFY(QTest::qWaitForWindowExposed(&view));
QSignalSpy spy1(&page1, &QWebEnginePage::loadFinished), spy2(&page2, &QWebEnginePage::loadFinished);
page1.setHtml("<html><body>"
"<a id='link' href='data:,hello'>link</a>"
"</body></html>");
QTRY_COMPARE(spy1.count(), 1);
QVERIFY(spy1.takeFirst().value(0).toBool());
page1.targetPage = &page2;
QTest::mouseClick(view.focusProxy(), Qt::MiddleButton, {}, elementCenter(&page1, "link"));
QTRY_COMPARE(spy2.count(), 1);
QVERIFY(spy2.takeFirst().value(0).toBool());
}
// What does createWindow do?
enum class OpenLinkInNewPageDecision {
// Returns nullptr,
ReturnNull,
// Returns this,
ReturnSelf,
// Returns page != this
ReturnOther,
};
// What causes createWindow to be called?
enum class OpenLinkInNewPageCause {
// User clicks on a link with target=_blank.
TargetBlank,
// User clicks with MiddleButton.
MiddleClick,
};
// What happens after createWindow?
enum class OpenLinkInNewPageEffect {
// The navigation request disappears into the ether.
Blocked,
// The navigation request becomes a navigation in the original page.
LoadInSelf,
// The navigation request becomes a navigation in a different page.
LoadInOther,
};
Q_DECLARE_METATYPE(OpenLinkInNewPageCause)
Q_DECLARE_METATYPE(OpenLinkInNewPageDecision)
Q_DECLARE_METATYPE(OpenLinkInNewPageEffect)
void tst_QWebEnginePage::openLinkInNewPage_data()
{
using Decision = OpenLinkInNewPageDecision;
using Cause = OpenLinkInNewPageCause;
using Effect = OpenLinkInNewPageEffect;
QTest::addColumn<Decision>("decision");
QTest::addColumn<Cause>("cause");
QTest::addColumn<Effect>("effect");
// Note that the meaning of returning nullptr from createWindow is not
// consistent between the TargetBlank and MiddleClick scenarios.
//
// With TargetBlank, the open-in-new-page disposition comes from the HTML
// target attribute; something the user is probably not aware of. Returning
// nullptr is interpreted as a decision by the app to block an unwanted
// popup.
//
// With MiddleClick, the open-in-new-page disposition comes from the user's
// explicit intent. Returning nullptr is then interpreted as a failure by
// the app to fulfill this intent, which we try to compensate by ignoring
// the disposition and performing the navigation request normally.
QTest::newRow("BlockPopup") << Decision::ReturnNull << Cause::TargetBlank << Effect::Blocked;
QTest::newRow("IgnoreIntent") << Decision::ReturnNull << Cause::MiddleClick << Effect::LoadInSelf;
QTest::newRow("OverridePopup") << Decision::ReturnSelf << Cause::TargetBlank << Effect::LoadInSelf;
QTest::newRow("OverrideIntent") << Decision::ReturnSelf << Cause::MiddleClick << Effect::LoadInSelf;
QTest::newRow("AcceptPopup") << Decision::ReturnOther << Cause::TargetBlank << Effect::LoadInOther;
QTest::newRow("AcceptIntent") << Decision::ReturnOther << Cause::MiddleClick << Effect::LoadInOther;
}
void tst_QWebEnginePage::openLinkInNewPage()
{
using Decision = OpenLinkInNewPageDecision;
using Cause = OpenLinkInNewPageCause;
using Effect = OpenLinkInNewPageEffect;
class Page : public QWebEnginePage
{
public:
Page *targetPage = nullptr;
QSignalSpy spy{this, &QWebEnginePage::loadFinished};
Page(QWebEngineProfile *profile) : QWebEnginePage(profile) {}
private:
QWebEnginePage *createWindow(WebWindowType) override { return targetPage; }
};
class View : public QWebEngineView
{
public:
View(Page *page)
{
resize(500, 500);
setPage(page);
}
};
QFETCH(Decision, decision);
QFETCH(Cause, cause);
QFETCH(Effect, effect);
QWebEngineProfile profile;
Page page1(&profile);
Page page2(&profile);
View view1(&page1);
View view2(&page2);
view1.show();
QVERIFY(QTest::qWaitForWindowExposed(&view1));
page1.setHtml("<html><body>"
"<a id='link' href='data:,hello' target='_blank'>link</a>"
"</body></html>");
QTRY_COMPARE(page1.spy.count(), 1);
QVERIFY(page1.spy.takeFirst().value(0).toBool());
switch (decision) {
case Decision::ReturnNull:
page1.targetPage = nullptr;
break;
case Decision::ReturnSelf:
page1.targetPage = &page1;
break;
case Decision::ReturnOther:
page1.targetPage = &page2;
break;
}
Qt::MouseButton button;
switch (cause) {
case Cause::TargetBlank:
button = Qt::LeftButton;
break;
case Cause::MiddleClick:
button = Qt::MiddleButton;
break;
}
QTest::mouseClick(view1.focusProxy(), button, {}, elementCenter(&page1, "link"));
switch (effect) {
case Effect::Blocked:
// Nothing to test
break;
case Effect::LoadInSelf:
QTRY_COMPARE(page1.spy.count(), 1);
QVERIFY(page1.spy.takeFirst().value(0).toBool());
QCOMPARE(page2.spy.count(), 0);
if (decision == Decision::ReturnSelf && cause == Cause::TargetBlank)
// History was discarded due to AddNewContents
QCOMPARE(page1.history()->count(), 1);
else
QCOMPARE(page1.history()->count(), 2);
QCOMPARE(page2.history()->count(), 0);
break;
case Effect::LoadInOther:
QTRY_COMPARE(page2.spy.count(), 1);
QVERIFY(page2.spy.takeFirst().value(0).toBool());
QCOMPARE(page1.spy.count(), 0);
QCOMPARE(page1.history()->count(), 1);
QCOMPARE(page2.history()->count(), 1);
break;
}
}
void tst_QWebEnginePage::triggerActionWithoutMenu()
{
// Calling triggerAction should not crash even when for
// context-menu-specific actions without a context menu.
QWebEngineProfile profile;
QWebEnginePage page(&profile);
page.triggerAction(QWebEnginePage::DownloadLinkToDisk);
}
void tst_QWebEnginePage::dynamicFrame()
{
QWebEnginePage page;
page.settings()->setAttribute(QWebEngineSettings::ErrorPageEnabled, false);
QSignalSpy spy(&page, &QWebEnginePage::loadFinished);
page.load(QStringLiteral("qrc:/resources/dynamicFrame.html"));
QTRY_COMPARE(spy.count(), 1);
QCOMPARE(toPlainTextSync(&page).trimmed(), QStringLiteral("foo"));
}
struct NotificationPage : ConsolePage {
Q_OBJECT
const QWebEnginePage::PermissionPolicy policy;
public:
NotificationPage(QWebEnginePage::PermissionPolicy ppolicy) : policy(ppolicy) {
connect(this, &QWebEnginePage::loadFinished, [load = spyLoad.ref()] (bool result) mutable { load(result); });
connect(this, &QWebEnginePage::featurePermissionRequested,
[this] (const QUrl &origin, QWebEnginePage::Feature feature) {
if (feature != QWebEnginePage::Notifications)
return;
if (spyRequest.wasCalled())
QFAIL("request executed twise!");
setFeaturePermission(origin, feature, policy);
spyRequest.ref()(origin);
});
load(QStringLiteral("qrc:///shared/notification.html"));
}
CallbackSpy<bool> spyLoad;
CallbackSpy<QUrl> spyRequest;
QString getPermission() { return evaluateJavaScriptSync(this, "getPermission()").toString(); }
void requestPermission() { runJavaScript("requestPermission()"); }
void resetPermission() { runJavaScript("resetPermission()"); }
void sendNotification(const QString &title, const QString &body) {
runJavaScript("sendNotification('" + title + "', '" + body + "')");
}
};
void tst_QWebEnginePage::notificationPermission_data()
{
QTest::addColumn<bool>("setOnInit");
QTest::addColumn<QWebEnginePage::PermissionPolicy>("policy");
QTest::addColumn<QString>("permission");
QTest::newRow("denyOnInit") << true << QWebEnginePage::PermissionDeniedByUser << "denied";
QTest::newRow("deny") << false << QWebEnginePage::PermissionDeniedByUser << "denied";
QTest::newRow("grant") << false << QWebEnginePage::PermissionGrantedByUser << "granted";
QTest::newRow("grantOnInit") << true << QWebEnginePage::PermissionGrantedByUser << "granted";
}
void tst_QWebEnginePage::notificationPermission()
{
QFETCH(bool, setOnInit);
QFETCH(QWebEnginePage::PermissionPolicy, policy);
QFETCH(QString, permission);
QWebEngineProfile otr;
QWebEnginePage page(&otr, nullptr);
QUrl baseUrl("https://www.example.com/somepage.html");
bool permissionRequested = false, errorState = false;
connect(&page, &QWebEnginePage::featurePermissionRequested, &page, [&] (const QUrl &o, QWebEnginePage::Feature f) {
if (f != QWebEnginePage::Notifications)
return;
if (permissionRequested || o != baseUrl.url(QUrl::RemoveFilename)) {
qWarning() << "Unexpected case. Can't proceed." << setOnInit << permissionRequested << o;
errorState = true;
return;
}
permissionRequested = true;
page.setFeaturePermission(o, f, policy);
});
if (setOnInit)
page.setFeaturePermission(baseUrl, QWebEnginePage::Notifications, policy);
QSignalSpy spy(&page, &QWebEnginePage::loadFinished);
page.setHtml(QString("<html><body>Test</body></html>"), baseUrl);
QTRY_COMPARE(spy.count(), 1);
QCOMPARE(evaluateJavaScriptSync(&page, QStringLiteral("Notification.permission")), setOnInit ? permission : QLatin1String("default"));
if (!setOnInit) {
page.setFeaturePermission(baseUrl, QWebEnginePage::Notifications, policy);
QTRY_COMPARE(evaluateJavaScriptSync(&page, QStringLiteral("Notification.permission")), permission);
}
auto js = QStringLiteral("var permission; Notification.requestPermission().then(p => { permission = p })");
evaluateJavaScriptSync(&page, js);
QTRY_COMPARE(evaluateJavaScriptSync(&page, "permission").toString(), permission);
// permission is not 'remembered' from api standpoint, hence is not suppressed on explicit call from JS
QVERIFY(permissionRequested);
QVERIFY(!errorState);
}
void tst_QWebEnginePage::sendNotification()
{
NotificationPage page(QWebEnginePage::PermissionGrantedByUser);
QVERIFY(page.spyLoad.waitForResult());
page.resetPermission();
page.requestPermission();
auto origin = page.spyRequest.waitForResult();
QVERIFY(page.spyRequest.wasCalled());
QCOMPARE(page.getPermission(), "granted");
std::unique_ptr<QWebEngineNotification> activeNotification;
CallbackSpy<bool> presenter;
page.profile()->setNotificationPresenter(
[&] (std::unique_ptr<QWebEngineNotification> notification)
{
activeNotification = std::move(notification);
presenter(true);
});
QString title("Title"), message("Message");
page.sendNotification(title, message);
presenter.waitForResult();
QVERIFY(presenter.wasCalled());
QVERIFY(activeNotification);
QCOMPARE(activeNotification->title(), title);
QCOMPARE(activeNotification->message(), message);
QCOMPARE(activeNotification->origin(), origin);
QCOMPARE(activeNotification->direction(), Qt::RightToLeft);
QCOMPARE(activeNotification->language(), "de");
QCOMPARE(activeNotification->tag(), "tst");
activeNotification->show();
QTRY_VERIFY2(page.messages.contains("onshow"), page.messages.join("\n").toLatin1().constData());
activeNotification->click();
QTRY_VERIFY2(page.messages.contains("onclick"), page.messages.join("\n").toLatin1().constData());
activeNotification->close();
QTRY_VERIFY2(page.messages.contains("onclose"), page.messages.join("\n").toLatin1().constData());
}
void tst_QWebEnginePage::contentsSize()
{
m_view->resize(800, 600);
m_view->show();
QSignalSpy loadSpy(m_page, &QWebEnginePage::loadFinished);
QSignalSpy contentsSizeChangedSpy(m_page, &QWebEnginePage::contentsSizeChanged);
m_view->setHtml(QString("<html><body style=\"width: 1600px; height: 1200px;\"><p>hi</p></body></html>"));
QTRY_COMPARE(loadSpy.count(), 1);
QTRY_COMPARE(contentsSizeChangedSpy.count(), 1);
// Verify the page's contents size is not limited by the view's size.
QCOMPARE(m_page->contentsSize().width(), 1608);
QCOMPARE(m_page->contentsSize().height(), 1216);
// Verify resizing the view does not affect the contents size.
m_view->resize(2400, 1800);
QCOMPARE(m_page->contentsSize().width(), 1608);
QCOMPARE(m_page->contentsSize().height(), 1216);
// Verify resizing the view does not affect the contents size.
m_view->resize(1600, 1200);
QCOMPARE(m_page->contentsSize().width(), 1608);
QCOMPARE(m_page->contentsSize().height(), 1216);
}
void tst_QWebEnginePage::setLifecycleState()
{
qRegisterMetaType<QWebEnginePage::LifecycleState>("LifecycleState");
QWebEngineProfile profile;
QWebEnginePage page(&profile);
QSignalSpy loadSpy(&page, &QWebEnginePage::loadFinished);
QSignalSpy lifecycleSpy(&page, &QWebEnginePage::lifecycleStateChanged);
QSignalSpy visibleSpy(&page, &QWebEnginePage::visibleChanged);
page.load(QStringLiteral("qrc:/resources/lifecycle.html"));
QTRY_COMPARE(loadSpy.count(), 1);
QCOMPARE(loadSpy.takeFirst().value(0), QVariant(true));
QCOMPARE(lifecycleSpy.count(), 0);
QCOMPARE(page.lifecycleState(), QWebEnginePage::LifecycleState::Active);
QCOMPARE(visibleSpy.count(), 0);
QCOMPARE(page.isVisible(), false);
QCOMPARE(evaluateJavaScriptSync(&page, "document.wasDiscarded"), QVariant(false));
QCOMPARE(evaluateJavaScriptSync(&page, "frozenness"), QVariant(0));
// Active -> Frozen
page.setLifecycleState(QWebEnginePage::LifecycleState::Frozen);
QCOMPARE(lifecycleSpy.count(), 1);
QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Frozen));
QCOMPARE(page.lifecycleState(), QWebEnginePage::LifecycleState::Frozen);
QCOMPARE(visibleSpy.count(), 0);
QCOMPARE(page.isVisible(), false);
QCOMPARE(evaluateJavaScriptSync(&page, "document.wasDiscarded"), QVariant(false));
QCOMPARE(evaluateJavaScriptSync(&page, "frozenness"), QVariant(1));
// Frozen -> Active
page.setLifecycleState(QWebEnginePage::LifecycleState::Active);
QCOMPARE(lifecycleSpy.count(), 1);
QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Active));
QCOMPARE(page.lifecycleState(), QWebEnginePage::LifecycleState::Active);
QCOMPARE(visibleSpy.count(), 0);
QCOMPARE(page.isVisible(), false);
QCOMPARE(evaluateJavaScriptSync(&page, "document.wasDiscarded"), QVariant(false));
QCOMPARE(evaluateJavaScriptSync(&page, "frozenness"), QVariant(0));
// Active -> Discarded
page.setLifecycleState(QWebEnginePage::LifecycleState::Discarded);
QCOMPARE(lifecycleSpy.count(), 1);
QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Discarded));
QCOMPARE(page.lifecycleState(), QWebEnginePage::LifecycleState::Discarded);
QCOMPARE(visibleSpy.count(), 0);
QCOMPARE(page.isVisible(), false);
QTest::ignoreMessage(QtWarningMsg, "runJavaScript: disabled in Discarded state");
QCOMPARE(evaluateJavaScriptSync(&page, "document.wasDiscarded"), QVariant());
QTest::ignoreMessage(QtWarningMsg, "runJavaScript: disabled in Discarded state");
QCOMPARE(evaluateJavaScriptSync(&page, "frozenness"), QVariant());
QCOMPARE(loadSpy.count(), 0);
// Discarded -> Frozen (illegal!)
QTest::ignoreMessage(QtWarningMsg,
"setLifecycleState: failed to transition from Discarded to Frozen state: "
"illegal transition");
page.setLifecycleState(QWebEnginePage::LifecycleState::Frozen);
QCOMPARE(lifecycleSpy.count(), 0);
QCOMPARE(page.lifecycleState(), QWebEnginePage::LifecycleState::Discarded);
// Discarded -> Active
page.setLifecycleState(QWebEnginePage::LifecycleState::Active);
QTRY_COMPARE(loadSpy.count(), 1);
QCOMPARE(loadSpy.takeFirst().value(0), QVariant(true));
QCOMPARE(lifecycleSpy.count(), 1);
QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Active));
QCOMPARE(page.lifecycleState(), QWebEnginePage::LifecycleState::Active);
QCOMPARE(visibleSpy.count(), 0);
QCOMPARE(page.isVisible(), false);
QCOMPARE(evaluateJavaScriptSync(&page, "document.wasDiscarded"), QVariant(true));
QCOMPARE(evaluateJavaScriptSync(&page, "frozenness"), QVariant(0));
// Active -> Frozen -> Discarded -> Active
page.setLifecycleState(QWebEnginePage::LifecycleState::Frozen);
page.setLifecycleState(QWebEnginePage::LifecycleState::Discarded);
page.setLifecycleState(QWebEnginePage::LifecycleState::Active);
QCOMPARE(lifecycleSpy.count(), 3);
QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Frozen));
QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Discarded));
QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Active));
QCOMPARE(page.lifecycleState(), QWebEnginePage::LifecycleState::Active);
QCOMPARE(visibleSpy.count(), 0);
QCOMPARE(page.isVisible(), false);
QTRY_COMPARE(loadSpy.count(), 1);
QCOMPARE(loadSpy.takeFirst().value(0), QVariant(true));
QCOMPARE(evaluateJavaScriptSync(&page, "document.wasDiscarded"), QVariant(true));
QCOMPARE(evaluateJavaScriptSync(&page, "frozenness"), QVariant(0));
// Reload clears document.wasDiscarded
page.triggerAction(QWebEnginePage::Reload);
QTRY_COMPARE(loadSpy.count(), 1);
QCOMPARE(loadSpy.takeFirst().value(0), QVariant(true));
QCOMPARE(evaluateJavaScriptSync(&page, "document.wasDiscarded"), QVariant(false));
}
void tst_QWebEnginePage::setVisible()
{
qRegisterMetaType<QWebEnginePage::LifecycleState>("LifecycleState");
QWebEngineProfile profile;
QWebEnginePage page(&profile);
QSignalSpy loadSpy(&page, &QWebEnginePage::loadFinished);
QSignalSpy lifecycleSpy(&page, &QWebEnginePage::lifecycleStateChanged);
QSignalSpy visibleSpy(&page, &QWebEnginePage::visibleChanged);
page.load(QStringLiteral("about:blank"));
QTRY_COMPARE(loadSpy.count(), 1);
QCOMPARE(loadSpy.takeFirst().value(0), QVariant(true));
QCOMPARE(lifecycleSpy.count(), 0);
QCOMPARE(page.lifecycleState(), QWebEnginePage::LifecycleState::Active);
QCOMPARE(visibleSpy.count(), 0);
QCOMPARE(page.isVisible(), false);
// hidden -> visible
page.setVisible(true);
QCOMPARE(lifecycleSpy.count(), 0);
QCOMPARE(page.lifecycleState(), QWebEnginePage::LifecycleState::Active);
QCOMPARE(visibleSpy.count(), 1);
QCOMPARE(visibleSpy.takeFirst().value(0), QVariant(true));
QCOMPARE(page.isVisible(), true);
// Active -> Frozen (illegal)
QTest::ignoreMessage(
QtWarningMsg,
"setLifecycleState: failed to transition from Active to Frozen state: page is visible");
page.setLifecycleState(QWebEnginePage::LifecycleState::Frozen);
QCOMPARE(lifecycleSpy.count(), 0);
// visible -> hidden
page.setVisible(false);
QCOMPARE(lifecycleSpy.count(), 0);
QCOMPARE(page.lifecycleState(), QWebEnginePage::LifecycleState::Active);
QCOMPARE(visibleSpy.count(), 1);
QCOMPARE(visibleSpy.takeFirst().value(0), QVariant(false));
QCOMPARE(page.isVisible(), false);
// Active -> Frozen
page.setLifecycleState(QWebEnginePage::LifecycleState::Frozen);
QCOMPARE(lifecycleSpy.count(), 1);
QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Frozen));
QCOMPARE(page.lifecycleState(), QWebEnginePage::LifecycleState::Frozen);
// hidden -> visible (triggers Frozen -> Active)
page.setVisible(true);
QCOMPARE(lifecycleSpy.count(), 1);
QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Active));
QCOMPARE(page.lifecycleState(), QWebEnginePage::LifecycleState::Active);
QCOMPARE(visibleSpy.count(), 1);
QCOMPARE(visibleSpy.takeFirst().value(0), QVariant(true));
QCOMPARE(page.isVisible(), true);
// Active -> Discarded (illegal)
QTest::ignoreMessage(QtWarningMsg,
"setLifecycleState: failed to transition from Active to Discarded state: "
"page is visible");
page.setLifecycleState(QWebEnginePage::LifecycleState::Discarded);
QCOMPARE(lifecycleSpy.count(), 0);
// visible -> hidden
page.setVisible(false);
QCOMPARE(lifecycleSpy.count(), 0);
QCOMPARE(page.lifecycleState(), QWebEnginePage::LifecycleState::Active);
QCOMPARE(visibleSpy.count(), 1);
QCOMPARE(visibleSpy.takeFirst().value(0), QVariant(false));
QCOMPARE(page.isVisible(), false);
// Active -> Discarded
page.setLifecycleState(QWebEnginePage::LifecycleState::Discarded);
QCOMPARE(lifecycleSpy.count(), 1);
QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Discarded));
QCOMPARE(page.lifecycleState(), QWebEnginePage::LifecycleState::Discarded);
// hidden -> visible (triggers Discarded -> Active)
page.setVisible(true);
QCOMPARE(lifecycleSpy.count(), 1);
QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Active));
QCOMPARE(page.lifecycleState(), QWebEnginePage::LifecycleState::Active);
QCOMPARE(visibleSpy.count(), 1);
QCOMPARE(visibleSpy.takeFirst().value(0), QVariant(true));
QCOMPARE(page.isVisible(), true);
QTRY_COMPARE(loadSpy.count(), 1);
QCOMPARE(loadSpy.takeFirst().value(0), QVariant(true));
}
void tst_QWebEnginePage::discardPreservesProperties()
{
QWebEngineProfile profile;
QWebEnginePage page(&profile);
QSignalSpy loadSpy(&page, &QWebEnginePage::loadFinished);
page.load(QStringLiteral("about:blank"));
QTRY_COMPARE(loadSpy.count(), 1);
QCOMPARE(loadSpy.takeFirst().value(0), QVariant(true));
// Change as many properties as possible to non-default values
bool audioMuted = true;
QVERIFY(page.isAudioMuted() != audioMuted);
page.setAudioMuted(audioMuted);
QColor backgroundColor = Qt::black;
QVERIFY(page.backgroundColor() != backgroundColor);
page.setBackgroundColor(backgroundColor);
qreal zoomFactor = 2;
QVERIFY(page.zoomFactor() != zoomFactor);
page.setZoomFactor(zoomFactor);
#if QT_CONFIG(webengine_webchannel)
QWebChannel *webChannel = new QWebChannel(&page);
page.setWebChannel(webChannel);
#endif
// Take snapshot of the rest
QSizeF contentsSize = page.contentsSize();
QIcon icon = page.icon();
QUrl iconUrl = page.iconUrl();
QUrl requestedUrl = page.requestedUrl();
QString title = page.title();
QUrl url = page.url();
// History should be preserved too
int historyCount = page.history()->count();
QCOMPARE(historyCount, 1);
int historyIndex = page.history()->currentItemIndex();
QCOMPARE(historyIndex, 0);
QWebEngineHistoryItem historyItem = page.history()->currentItem();
QVERIFY(historyItem.isValid());
// Discard + undiscard
page.setLifecycleState(QWebEnginePage::LifecycleState::Discarded);
page.setLifecycleState(QWebEnginePage::LifecycleState::Active);
QTRY_COMPARE(loadSpy.count(), 1);
QCOMPARE(loadSpy.takeFirst().value(0), QVariant(true));
// Property changes should be preserved
QCOMPARE(page.isAudioMuted(), audioMuted);
QCOMPARE(page.backgroundColor(), backgroundColor);
QCOMPARE(page.contentsSize(), contentsSize);
QCOMPARE(page.icon(), icon);
QCOMPARE(page.iconUrl(), iconUrl);
QCOMPARE(page.requestedUrl(), requestedUrl);
QCOMPARE(page.title(), title);
QCOMPARE(page.url(), url);
QCOMPARE(page.zoomFactor(), zoomFactor);
#if QT_CONFIG(webengine_webchannel)
QCOMPARE(page.webChannel(), webChannel);
#endif
QCOMPARE(page.history()->count(), historyCount);
QCOMPARE(page.history()->currentItemIndex(), historyIndex);
QCOMPARE(page.history()->currentItem().url(), historyItem.url());
QCOMPARE(page.history()->currentItem().originalUrl(), historyItem.originalUrl());
QCOMPARE(page.history()->currentItem().title(), historyItem.title());
}
void tst_QWebEnginePage::discardBeforeInitialization()
{
QWebEngineProfile profile;
QWebEnginePage page(&profile);
page.setLifecycleState(QWebEnginePage::LifecycleState::Discarded);
// The call is ignored
QCOMPARE(page.lifecycleState(), QWebEnginePage::LifecycleState::Active);
}
void tst_QWebEnginePage::automaticUndiscard()
{
QWebEngineProfile profile;
QWebEnginePage page(&profile);
QSignalSpy loadSpy(&page, &QWebEnginePage::loadFinished);
page.load(QStringLiteral("about:blank"));
QTRY_COMPARE(loadSpy.count(), 1);
QCOMPARE(loadSpy.takeFirst().value(0), QVariant(true));
// setUrl
page.setLifecycleState(QWebEnginePage::LifecycleState::Discarded);
page.setUrl(QStringLiteral("qrc:/resources/lifecycle.html"));
QCOMPARE(page.lifecycleState(), QWebEnginePage::LifecycleState::Active);
// setContent
page.setLifecycleState(QWebEnginePage::LifecycleState::Discarded);
page.setContent(QByteArrayLiteral("foo"));
QCOMPARE(page.lifecycleState(), QWebEnginePage::LifecycleState::Active);
}
void tst_QWebEnginePage::setLifecycleStateWithDevTools()
{
QWebEngineProfile profile;
QWebEnginePage inspectedPage(&profile);
QWebEnginePage devToolsPage(&profile);
QSignalSpy devToolsSpy(&devToolsPage, &QWebEnginePage::loadFinished);
QSignalSpy inspectedSpy(&inspectedPage, &QWebEnginePage::loadFinished);
// Ensure pages are initialized
inspectedPage.load(QStringLiteral("about:blank"));
devToolsPage.load(QStringLiteral("about:blank"));
QTRY_COMPARE_WITH_TIMEOUT(inspectedSpy.count(), 1, 30000);
QCOMPARE(inspectedSpy.takeFirst().value(0), QVariant(true));
QTRY_COMPARE_WITH_TIMEOUT(devToolsSpy.count(), 1, 30000);
QCOMPARE(devToolsSpy.takeFirst().value(0), QVariant(true));
// Open DevTools with Frozen inspectedPage
inspectedPage.setLifecycleState(QWebEnginePage::LifecycleState::Frozen);
inspectedPage.setDevToolsPage(&devToolsPage);
QCOMPARE(inspectedPage.lifecycleState(), QWebEnginePage::LifecycleState::Active);
QTRY_COMPARE(devToolsSpy.count(), 1);
QCOMPARE(devToolsSpy.takeFirst().value(0), QVariant(true));
inspectedPage.setDevToolsPage(nullptr);
// Open DevTools with Discarded inspectedPage
inspectedPage.setLifecycleState(QWebEnginePage::LifecycleState::Discarded);
inspectedPage.setDevToolsPage(&devToolsPage);
QCOMPARE(inspectedPage.lifecycleState(), QWebEnginePage::LifecycleState::Active);
QTRY_COMPARE(devToolsSpy.count(), 1);
QCOMPARE(devToolsSpy.takeFirst().value(0), QVariant(true));
QTRY_COMPARE(inspectedSpy.count(), 1);
QCOMPARE(inspectedSpy.takeFirst().value(0), QVariant(true));
inspectedPage.setDevToolsPage(nullptr);
// Open DevTools with Frozen devToolsPage
devToolsPage.setLifecycleState(QWebEnginePage::LifecycleState::Frozen);
devToolsPage.setInspectedPage(&inspectedPage);
QCOMPARE(devToolsPage.lifecycleState(), QWebEnginePage::LifecycleState::Active);
QTRY_COMPARE(devToolsSpy.count(), 1);
QCOMPARE(devToolsSpy.takeFirst().value(0), QVariant(true));
devToolsPage.setInspectedPage(nullptr);
// Open DevTools with Discarded devToolsPage
devToolsPage.setLifecycleState(QWebEnginePage::LifecycleState::Discarded);
devToolsPage.setInspectedPage(&inspectedPage);
QCOMPARE(devToolsPage.lifecycleState(), QWebEnginePage::LifecycleState::Active);
QTRY_COMPARE(devToolsSpy.count(), 2);
QCOMPARE(devToolsSpy.takeFirst().value(0), QVariant(false));
QCOMPARE(devToolsSpy.takeFirst().value(0), QVariant(true));
// keep DevTools open
// Try to change state while DevTools are open
QTest::ignoreMessage(
QtWarningMsg,
"setLifecycleState: failed to transition from Active to Frozen state: DevTools open");
inspectedPage.setLifecycleState(QWebEnginePage::LifecycleState::Frozen);
QCOMPARE(inspectedPage.lifecycleState(), QWebEnginePage::LifecycleState::Active);
QTest::ignoreMessage(QtWarningMsg,
"setLifecycleState: failed to transition from Active to Discarded state: "
"DevTools open");
inspectedPage.setLifecycleState(QWebEnginePage::LifecycleState::Discarded);
QCOMPARE(inspectedPage.lifecycleState(), QWebEnginePage::LifecycleState::Active);
QTest::ignoreMessage(
QtWarningMsg,
"setLifecycleState: failed to transition from Active to Frozen state: DevTools open");
devToolsPage.setLifecycleState(QWebEnginePage::LifecycleState::Frozen);
QCOMPARE(devToolsPage.lifecycleState(), QWebEnginePage::LifecycleState::Active);
QTest::ignoreMessage(QtWarningMsg,
"setLifecycleState: failed to transition from Active to Discarded state: "
"DevTools open");
devToolsPage.setLifecycleState(QWebEnginePage::LifecycleState::Discarded);
QCOMPARE(devToolsPage.lifecycleState(), QWebEnginePage::LifecycleState::Active);
}
void tst_QWebEnginePage::discardPreservesCommittedLoad()
{
QWebEngineProfile profile;
QWebEnginePage page(&profile);
QSignalSpy loadStartedSpy(&page, &QWebEnginePage::loadStarted);
QSignalSpy loadFinishedSpy(&page, &QWebEnginePage::loadFinished);
QSignalSpy urlChangedSpy(&page, &QWebEnginePage::urlChanged);
QSignalSpy titleChangedSpy(&page, &QWebEnginePage::titleChanged);
QString url = QStringLiteral("qrc:/resources/lifecycle.html");
page.setUrl(url);
QTRY_COMPARE(loadStartedSpy.count(), 1);
loadStartedSpy.clear();
QTRY_COMPARE(loadFinishedSpy.count(), 1);
QCOMPARE(loadFinishedSpy.takeFirst().value(0), QVariant(true));
QCOMPARE(urlChangedSpy.count(), 1);
QCOMPARE(urlChangedSpy.takeFirst().value(0), QVariant(QUrl(url)));
QCOMPARE(page.url(), url);
QCOMPARE(titleChangedSpy.count(), 2);
QCOMPARE(titleChangedSpy.takeFirst().value(0), QVariant(url));
QString title = QStringLiteral("Lifecycle");
QCOMPARE(titleChangedSpy.takeFirst().value(0), QVariant(title));
QCOMPARE(page.title(), title);
page.setLifecycleState(QWebEnginePage::LifecycleState::Discarded);
QCOMPARE(loadStartedSpy.count(), 0);
QCOMPARE(loadFinishedSpy.count(), 0);
QCOMPARE(urlChangedSpy.count(), 0);
QCOMPARE(page.url(), QUrl(url));
QCOMPARE(titleChangedSpy.count(), 0);
QCOMPARE(page.title(), title);
page.setLifecycleState(QWebEnginePage::LifecycleState::Active);
QTRY_COMPARE(loadStartedSpy.count(), 1);
loadStartedSpy.clear();
QTRY_COMPARE(loadFinishedSpy.count(), 1);
QCOMPARE(loadFinishedSpy.takeFirst().value(0), QVariant(true));
QCOMPARE(urlChangedSpy.count(), 0);
QCOMPARE(page.url(), url);
QCOMPARE(titleChangedSpy.count(), 0);
QCOMPARE(page.title(), title);
}
void tst_QWebEnginePage::discardAbortsPendingLoad()
{
QWebEngineProfile profile;
QWebEnginePage page(&profile);
QSignalSpy loadStartedSpy(&page, &QWebEnginePage::loadStarted);
QSignalSpy loadFinishedSpy(&page, &QWebEnginePage::loadFinished);
QSignalSpy urlChangedSpy(&page, &QWebEnginePage::urlChanged);
QSignalSpy titleChangedSpy(&page, &QWebEnginePage::titleChanged);
connect(&page, &QWebEnginePage::loadStarted,
[&]() { page.setLifecycleState(QWebEnginePage::LifecycleState::Discarded); });
QUrl url = QStringLiteral("qrc:/resources/lifecycle.html");
page.setUrl(url);
QTRY_COMPARE(loadStartedSpy.count(), 1);
loadStartedSpy.clear();
QTRY_COMPARE(loadFinishedSpy.count(), 1);
QCOMPARE(loadFinishedSpy.takeFirst().value(0), QVariant(false));
QCOMPARE(urlChangedSpy.count(), 2);
QCOMPARE(urlChangedSpy.takeFirst().value(0), QVariant(url));
QCOMPARE(urlChangedSpy.takeFirst().value(0), QVariant(QUrl()));
QCOMPARE(titleChangedSpy.count(), 0);
QCOMPARE(page.url(), QUrl());
QCOMPARE(page.title(), QString());
page.setLifecycleState(QWebEnginePage::LifecycleState::Active);
QCOMPARE(loadStartedSpy.count(), 0);
QCOMPARE(loadFinishedSpy.count(), 0);
QCOMPARE(urlChangedSpy.count(), 0);
QCOMPARE(page.url(), QUrl());
QCOMPARE(page.title(), QString());
}
void tst_QWebEnginePage::discardAbortsPendingLoadAndPreservesCommittedLoad()
{
QWebEngineProfile profile;
QWebEnginePage page(&profile);
QSignalSpy loadStartedSpy(&page, &QWebEnginePage::loadStarted);
QSignalSpy loadFinishedSpy(&page, &QWebEnginePage::loadFinished);
QSignalSpy urlChangedSpy(&page, &QWebEnginePage::urlChanged);
QSignalSpy titleChangedSpy(&page, &QWebEnginePage::titleChanged);
QString url1 = QStringLiteral("qrc:/resources/lifecycle.html");
page.setUrl(url1);
QTRY_COMPARE(loadStartedSpy.count(), 1);
loadStartedSpy.clear();
QTRY_COMPARE(loadFinishedSpy.count(), 1);
QCOMPARE(loadFinishedSpy.takeFirst().value(0), QVariant(true));
QCOMPARE(urlChangedSpy.count(), 1);
QCOMPARE(urlChangedSpy.takeFirst().value(0), QVariant(QUrl(url1)));
QCOMPARE(page.url(), url1);
QCOMPARE(titleChangedSpy.count(), 2);
QCOMPARE(titleChangedSpy.takeFirst().value(0), QVariant(url1));
QString title = QStringLiteral("Lifecycle");
QCOMPARE(titleChangedSpy.takeFirst().value(0), QVariant(title));
QCOMPARE(page.title(), title);
connect(&page, &QWebEnginePage::loadStarted,
[&]() { page.setLifecycleState(QWebEnginePage::LifecycleState::Discarded); });
QString url2 = QStringLiteral("about:blank");
page.setUrl(url2);
QTRY_COMPARE(loadStartedSpy.count(), 1);
loadStartedSpy.clear();
QTRY_COMPARE(loadFinishedSpy.count(), 1);
QCOMPARE(loadFinishedSpy.takeFirst().value(0), QVariant(false));
QCOMPARE(urlChangedSpy.count(), 2);
QCOMPARE(urlChangedSpy.takeFirst().value(0), QVariant(QUrl(url2)));
QCOMPARE(urlChangedSpy.takeFirst().value(0), QVariant(QUrl(url1)));
QCOMPARE(titleChangedSpy.count(), 0);
QCOMPARE(page.url(), url1);
QCOMPARE(page.title(), title);
page.setLifecycleState(QWebEnginePage::LifecycleState::Active);
QCOMPARE(loadStartedSpy.count(), 0);
QCOMPARE(loadFinishedSpy.count(), 0);
QCOMPARE(urlChangedSpy.count(), 0);
QCOMPARE(page.url(), url1);
QCOMPARE(page.title(), title);
}
void tst_QWebEnginePage::recommendedState()
{
qRegisterMetaType<QWebEnginePage::LifecycleState>("LifecycleState");
QWebEngineProfile profile;
QWebEnginePage page(&profile);
struct Event {
enum { StateChange, RecommendationChange } key;
QWebEnginePage::LifecycleState value;
};
std::vector<Event> events;
connect(&page, &QWebEnginePage::lifecycleStateChanged, [&](QWebEnginePage::LifecycleState state) {
events.push_back(Event { Event::StateChange, state });
});
connect(&page, &QWebEnginePage::recommendedStateChanged, [&](QWebEnginePage::LifecycleState state) {
events.push_back(Event { Event::RecommendationChange, state });
});
page.load(QStringLiteral("qrc:/resources/lifecycle.html"));
QTRY_COMPARE(events.size(), 1u);
QCOMPARE(events[0].key, Event::RecommendationChange);
QCOMPARE(events[0].value, QWebEnginePage::LifecycleState::Frozen);
events.clear();
QCOMPARE(page.recommendedState(), QWebEnginePage::LifecycleState::Frozen);
page.setVisible(true);
QTRY_COMPARE(events.size(), 1u);
QCOMPARE(events[0].key, Event::RecommendationChange);
QCOMPARE(events[0].value, QWebEnginePage::LifecycleState::Active);
events.clear();
QCOMPARE(page.recommendedState(), QWebEnginePage::LifecycleState::Active);
page.setVisible(false);
QTRY_COMPARE(events.size(), 1u);
QCOMPARE(events[0].key, Event::RecommendationChange);
QCOMPARE(events[0].value, QWebEnginePage::LifecycleState::Frozen);
events.clear();
QCOMPARE(page.recommendedState(), QWebEnginePage::LifecycleState::Frozen);
page.triggerAction(QWebEnginePage::Reload);
QTRY_COMPARE(events.size(), 2u);
QCOMPARE(events[0].key, Event::RecommendationChange);
QCOMPARE(events[0].value, QWebEnginePage::LifecycleState::Active);
QCOMPARE(events[1].key, Event::RecommendationChange);
QCOMPARE(events[1].value, QWebEnginePage::LifecycleState::Frozen);
events.clear();
QCOMPARE(page.recommendedState(), QWebEnginePage::LifecycleState::Frozen);
QWebEnginePage devTools;
page.setDevToolsPage(&devTools);
QTRY_COMPARE(events.size(), 1u);
QCOMPARE(events[0].key, Event::RecommendationChange);
QCOMPARE(events[0].value, QWebEnginePage::LifecycleState::Active);
events.clear();
QCOMPARE(page.recommendedState(), QWebEnginePage::LifecycleState::Active);
page.setDevToolsPage(nullptr);
QTRY_COMPARE(events.size(), 1u);
QCOMPARE(events[0].key, Event::RecommendationChange);
QCOMPARE(events[0].value, QWebEnginePage::LifecycleState::Frozen);
events.clear();
QCOMPARE(page.recommendedState(), QWebEnginePage::LifecycleState::Frozen);
page.setLifecycleState(QWebEnginePage::LifecycleState::Frozen);
QTRY_COMPARE(events.size(), 2u);
QCOMPARE(events[0].key, Event::StateChange);
QCOMPARE(events[0].value, QWebEnginePage::LifecycleState::Frozen);
QCOMPARE(events[1].key, Event::RecommendationChange);
QCOMPARE(events[1].value, QWebEnginePage::LifecycleState::Discarded);
events.clear();
QCOMPARE(page.recommendedState(), QWebEnginePage::LifecycleState::Discarded);
page.setLifecycleState(QWebEnginePage::LifecycleState::Discarded);
QTRY_COMPARE(events.size(), 1u);
QCOMPARE(events[0].key, Event::StateChange);
QCOMPARE(events[0].value, QWebEnginePage::LifecycleState::Discarded);
events.clear();
QCOMPARE(page.recommendedState(), QWebEnginePage::LifecycleState::Discarded);
}
void tst_QWebEnginePage::recommendedStateAuto()
{
qRegisterMetaType<QWebEnginePage::LifecycleState>("LifecycleState");
QWebEngineProfile profile;
QWebEnginePage page(&profile);
QSignalSpy lifecycleSpy(&page, &QWebEnginePage::lifecycleStateChanged);
connect(&page, &QWebEnginePage::recommendedStateChanged, &page, &QWebEnginePage::setLifecycleState);
page.load(QStringLiteral("qrc:/resources/lifecycle.html"));
QTRY_COMPARE(lifecycleSpy.count(), 2);
QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Frozen));
QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Discarded));
page.setVisible(true);
QTRY_COMPARE(lifecycleSpy.count(), 1);
QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Active));
page.setVisible(false);
QTRY_COMPARE(lifecycleSpy.count(), 2);
QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Frozen));
QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Discarded));
page.triggerAction(QWebEnginePage::Reload);
QTRY_COMPARE(lifecycleSpy.count(), 3);
QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Active));
QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Frozen));
QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Discarded));
QWebEnginePage devTools;
page.setDevToolsPage(&devTools);
QTRY_COMPARE(lifecycleSpy.count(), 1);
QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Active));
page.setDevToolsPage(nullptr);
QTRY_COMPARE(lifecycleSpy.count(), 2);
QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Frozen));
QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Discarded));
}
void tst_QWebEnginePage::setLifecycleStateAndReload()
{
qRegisterMetaType<QWebEnginePage::LifecycleState>("LifecycleState");
QWebEngineProfile profile;
QWebEnginePage page(&profile);
QSignalSpy loadSpy(&page, &QWebEnginePage::loadFinished);
QSignalSpy lifecycleSpy(&page, &QWebEnginePage::lifecycleStateChanged);
page.load(QStringLiteral("qrc:/resources/lifecycle.html"));
QTRY_COMPARE(loadSpy.count(), 1);
QCOMPARE(loadSpy.takeFirst().value(0), QVariant(true));
QCOMPARE(lifecycleSpy.count(), 0);
QCOMPARE(page.lifecycleState(), QWebEnginePage::LifecycleState::Active);
page.setLifecycleState(QWebEnginePage::LifecycleState::Frozen);
QCOMPARE(page.lifecycleState(), QWebEnginePage::LifecycleState::Frozen);
QCOMPARE(lifecycleSpy.count(), 1);
QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Frozen));
page.triggerAction(QWebEnginePage::Reload);
QCOMPARE(page.lifecycleState(), QWebEnginePage::LifecycleState::Active);
QCOMPARE(lifecycleSpy.count(), 1);
QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Active));
QTRY_COMPARE(loadSpy.count(), 1);
QCOMPARE(loadSpy.takeFirst().value(0), QVariant(true));
page.setLifecycleState(QWebEnginePage::LifecycleState::Discarded);
QCOMPARE(page.lifecycleState(), QWebEnginePage::LifecycleState::Discarded);
QCOMPARE(lifecycleSpy.count(), 1);
QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Discarded));
page.triggerAction(QWebEnginePage::Reload);
QCOMPARE(page.lifecycleState(), QWebEnginePage::LifecycleState::Active);
QCOMPARE(lifecycleSpy.count(), 1);
QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Active));
QTRY_COMPARE(loadSpy.count(), 1);
QCOMPARE(loadSpy.takeFirst().value(0), QVariant(true));
}
void tst_QWebEnginePage::editActionsWithExplicitFocus()
{
QWebEngineView view;
QWebEnginePage *page = view.page();
view.settings()->setAttribute(QWebEngineSettings::FocusOnNavigationEnabled, false);
QSignalSpy loadFinishedSpy(page, &QWebEnginePage::loadFinished);
QSignalSpy selectionChangedSpy(page, &QWebEnginePage::selectionChanged);
QSignalSpy actionChangedSpy(page->action(QWebEnginePage::SelectAll), &QAction::changed);
// The view is hidden and no focus on the page. Edit actions should be disabled.
QVERIFY(!view.isVisible());
QVERIFY(!page->action(QWebEnginePage::SelectAll)->isEnabled());
page->setHtml(QString("<html><body><div>foo bar</div></body></html>"));
QTRY_COMPARE(loadFinishedSpy.count(), 1);
// Still no focus because focus on navigation is disabled. Edit actions don't do anything (should not crash).
QVERIFY(!page->action(QWebEnginePage::SelectAll)->isEnabled());
view.page()->triggerAction(QWebEnginePage::SelectAll);
QCOMPARE(selectionChangedSpy.count(), 0);
QCOMPARE(page->hasSelection(), false);
// Focus content by focusing window from JavaScript. Edit actions should be enabled and functional.
evaluateJavaScriptSync(page, "window.focus();");
QTRY_COMPARE(actionChangedSpy.count(), 1);
QVERIFY(page->action(QWebEnginePage::SelectAll)->isEnabled());
view.page()->triggerAction(QWebEnginePage::SelectAll);
QTRY_COMPARE(selectionChangedSpy.count(), 1);
QCOMPARE(page->hasSelection(), true);
QCOMPARE(page->selectedText(), QStringLiteral("foo bar"));
}
void tst_QWebEnginePage::editActionsWithInitialFocus()
{
QWebEngineView view;
QWebEnginePage *page = view.page();
view.settings()->setAttribute(QWebEngineSettings::FocusOnNavigationEnabled, true);
QSignalSpy loadFinishedSpy(page, &QWebEnginePage::loadFinished);
QSignalSpy selectionChangedSpy(page, &QWebEnginePage::selectionChanged);
QSignalSpy actionChangedSpy(page->action(QWebEnginePage::SelectAll), &QAction::changed);
// The view is hidden and no focus on the page. Edit actions should be disabled.
QVERIFY(!view.isVisible());
QVERIFY(!page->action(QWebEnginePage::SelectAll)->isEnabled());
page->setHtml(QString("<html><body><div>foo bar</div></body></html>"));
QTRY_COMPARE(loadFinishedSpy.count(), 1);
// Content gets initial focus.
QTRY_COMPARE(actionChangedSpy.count(), 1);
QVERIFY(page->action(QWebEnginePage::SelectAll)->isEnabled());
view.page()->triggerAction(QWebEnginePage::SelectAll);
QTRY_COMPARE(selectionChangedSpy.count(), 1);
QCOMPARE(page->hasSelection(), true);
QCOMPARE(page->selectedText(), QStringLiteral("foo bar"));
}
void tst_QWebEnginePage::editActionsWithFocusOnIframe()
{
QWebEngineView view;
QWebEnginePage *page = view.page();
view.settings()->setAttribute(QWebEngineSettings::FocusOnNavigationEnabled, false);
QSignalSpy loadFinishedSpy(page, &QWebEnginePage::loadFinished);
QSignalSpy selectionChangedSpy(page, &QWebEnginePage::selectionChanged);
QSignalSpy actionChangedSpy(page->action(QWebEnginePage::SelectAll), &QAction::changed);
// The view is hidden and no focus on the page. Edit actions should be disabled.
QVERIFY(!view.isVisible());
QVERIFY(!page->action(QWebEnginePage::SelectAll)->isEnabled());
page->load(QUrl("qrc:///resources/iframe2.html"));
QTRY_COMPARE(loadFinishedSpy.count(), 1);
QVERIFY(!page->action(QWebEnginePage::SelectAll)->isEnabled());
// Focusing an iframe.
evaluateJavaScriptSync(page, "document.getElementsByTagName('iframe')[0].contentWindow.focus()");
QTRY_COMPARE(actionChangedSpy.count(), 1);
QVERIFY(page->action(QWebEnginePage::SelectAll)->isEnabled());
view.page()->triggerAction(QWebEnginePage::SelectAll);
QTRY_COMPARE(selectionChangedSpy.count(), 1);
QCOMPARE(page->hasSelection(), true);
QCOMPARE(page->selectedText(), QStringLiteral("inner"));
}
void tst_QWebEnginePage::editActionsWithoutSelection()
{
QWebEngineView view;
QWebEnginePage *page = view.page();
view.settings()->setAttribute(QWebEngineSettings::FocusOnNavigationEnabled, true);
QSignalSpy loadFinishedSpy(page, &QWebEnginePage::loadFinished);
QSignalSpy selectionChangedSpy(page, &QWebEnginePage::selectionChanged);
QSignalSpy actionChangedSpy(page->action(QWebEnginePage::SelectAll), &QAction::changed);
page->setHtml(QString("<html><body><div>foo bar</div></body></html>"));
QTRY_COMPARE(loadFinishedSpy.count(), 1);
QTRY_COMPARE(actionChangedSpy.count(), 1);
QVERIFY(!page->action(QWebEnginePage::Cut)->isEnabled());
QVERIFY(!page->action(QWebEnginePage::Copy)->isEnabled());
QVERIFY(page->action(QWebEnginePage::Paste)->isEnabled());
QVERIFY(page->action(QWebEnginePage::Undo)->isEnabled());
QVERIFY(page->action(QWebEnginePage::Redo)->isEnabled());
QVERIFY(page->action(QWebEnginePage::SelectAll)->isEnabled());
QVERIFY(page->action(QWebEnginePage::PasteAndMatchStyle)->isEnabled());
QVERIFY(!page->action(QWebEnginePage::Unselect)->isEnabled());
page->triggerAction(QWebEnginePage::SelectAll);
QTRY_COMPARE(selectionChangedSpy.count(), 1);
QCOMPARE(page->hasSelection(), true);
QCOMPARE(page->selectedText(), QStringLiteral("foo bar"));
QVERIFY(page->action(QWebEnginePage::Cut)->isEnabled());
QVERIFY(page->action(QWebEnginePage::Copy)->isEnabled());
QVERIFY(page->action(QWebEnginePage::Paste)->isEnabled());
QVERIFY(page->action(QWebEnginePage::Undo)->isEnabled());
QVERIFY(page->action(QWebEnginePage::Redo)->isEnabled());
QVERIFY(page->action(QWebEnginePage::SelectAll)->isEnabled());
QVERIFY(page->action(QWebEnginePage::PasteAndMatchStyle)->isEnabled());
QVERIFY(page->action(QWebEnginePage::Unselect)->isEnabled());
}
void tst_QWebEnginePage::customUserAgentInNewTab()
{
HttpServer server;
QByteArray lastUserAgent;
connect(&server, &HttpServer::newRequest, [&](HttpReqRep *rr) {
QCOMPARE(rr->requestMethod(), "GET");
lastUserAgent = rr->requestHeader("user-agent");
rr->setResponseBody(QByteArrayLiteral("<html><body>Test</body></html>"));
rr->sendResponse();
});
QVERIFY(server.start());
class Page : public QWebEnginePage {
public:
QWebEngineProfile *targetProfile = nullptr;
QScopedPointer<QWebEnginePage> newPage;
Page(QWebEngineProfile *profile) : QWebEnginePage(profile) {}
private:
QWebEnginePage *createWindow(WebWindowType) override
{
newPage.reset(new QWebEnginePage(targetProfile ? targetProfile : profile(), nullptr));
return newPage.data();
}
};
QWebEngineProfile profile1, profile2;
profile1.setHttpUserAgent(QStringLiteral("custom 1"));
profile2.setHttpUserAgent(QStringLiteral("custom 2"));
Page page(&profile1);
QWebEngineView view;
view.resize(500, 500);
view.setPage(&page);
view.show();
QVERIFY(QTest::qWaitForWindowExposed(&view));
QSignalSpy spy(&page, &QWebEnginePage::loadFinished);
// First check we can get the user-agent passed through normally
page.setHtml(QString("<html><body><a id='link' target='_blank' href='") +
server.url("/test1").toEncoded() +
QString("'>link</a></body></html>"));
QTRY_COMPARE(spy.count(), 1);
QVERIFY(spy.takeFirst().value(0).toBool());
QCOMPARE(evaluateJavaScriptSync(&page, QStringLiteral("navigator.userAgent")).toString(), profile1.httpUserAgent());
QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, elementCenter(&page, "link"));
QTRY_VERIFY(page.newPage);
QTRY_VERIFY(!lastUserAgent.isEmpty());
QCOMPARE(lastUserAgent, profile1.httpUserAgent().toUtf8());
// Now check we can get the new user-agent of the profile
page.newPage.reset();
page.targetProfile = &profile2;
spy.clear();
lastUserAgent = { };
page.setHtml(QString("<html><body><a id='link' target='_blank' href='") +
server.url("/test2").toEncoded() +
QString("'>link</a></body></html>"));
QTRY_COMPARE(spy.count(), 1);
QVERIFY(spy.takeFirst().value(0).toBool());
QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, elementCenter(&page, "link"));
QTRY_VERIFY(page.newPage);
QTRY_VERIFY(!lastUserAgent.isEmpty());
QCOMPARE(lastUserAgent, profile2.httpUserAgent().toUtf8());
}
void tst_QWebEnginePage::renderProcessCrashed()
{
using Status = QWebEnginePage::RenderProcessTerminationStatus;
QWebEngineProfile profile;
QWebEnginePage page(&profile);
bool done = false;
Status status;
connect(&page, &QWebEnginePage::renderProcessTerminated, [&](Status newStatus) {
status = newStatus;
done = true;
});
page.load(QUrl("chrome://crash"));
QTRY_VERIFY_WITH_TIMEOUT(done, 20000);
// The status depends on whether stack traces are enabled. With
// --disable-in-process-stack-traces we get an AbnormalTerminationStatus,
// otherwise a CrashedTerminationStatus.
QVERIFY(status == QWebEnginePage::CrashedTerminationStatus ||
status == QWebEnginePage::AbnormalTerminationStatus);
}
void tst_QWebEnginePage::renderProcessPid()
{
QCOMPARE(m_page->renderProcessPid(), 0);
m_page->load(QUrl("about:blank"));
QSignalSpy spyFinished(m_page, &QWebEnginePage::loadFinished);
QVERIFY(spyFinished.wait());
QVERIFY(m_page->renderProcessPid() > 1);
bool crashed = false;
connect(m_page, &QWebEnginePage::renderProcessTerminated, [&]() { crashed = true; });
m_page->load(QUrl("chrome://crash"));
QTRY_VERIFY_WITH_TIMEOUT(crashed, 20000);
QCOMPARE(m_page->renderProcessPid(), 0);
}
void tst_QWebEnginePage::backgroundColor()
{
QWebEngineProfile profile;
QWebEngineView view;
QWebEnginePage *page = new QWebEnginePage(&profile, &view);
view.resize(640, 480);
view.setStyleSheet("background: yellow");
view.show();
QPoint center(view.size().width() / 2, view.size().height() / 2);
QCOMPARE(page->backgroundColor(), Qt::white);
QTRY_COMPARE(view.grab().toImage().pixelColor(center), Qt::white);
page->setBackgroundColor(Qt::red);
view.setPage(page);
QCOMPARE(page->backgroundColor(), Qt::red);
QTRY_COMPARE(view.grab().toImage().pixelColor(center), Qt::red);
page->setHtml(QString("<html>"
"<head><style>html, body { margin:0; padding:0; }</style></head>"
"<body><div style=\"width:100%; height:10px; background-color:black\"/></body>"
"</html>"));
QSignalSpy spyFinished(page, &QWebEnginePage::loadFinished);
QVERIFY(spyFinished.wait());
// Make sure the page is rendered and the test is not grabbing the color of the RenderWidgetHostViewQtDelegateWidget.
QTRY_COMPARE(view.grab().toImage().pixelColor(QPoint(5, 5)), Qt::black);
QCOMPARE(page->backgroundColor(), Qt::red);
QCOMPARE(view.grab().toImage().pixelColor(center), Qt::red);
page->setBackgroundColor(Qt::transparent);
QCOMPARE(page->backgroundColor(), Qt::transparent);
QTRY_COMPARE(view.grab().toImage().pixelColor(center), Qt::yellow);
page->setBackgroundColor(Qt::green);
QCOMPARE(page->backgroundColor(), Qt::green);
QTRY_COMPARE(view.grab().toImage().pixelColor(center), Qt::green);
}
void tst_QWebEnginePage::audioMuted()
{
QWebEngineProfile profile;
QWebEnginePage page(&profile);
QSignalSpy spy(&page, &QWebEnginePage::audioMutedChanged);
QCOMPARE(page.isAudioMuted(), false);
page.setAudioMuted(true);
loadSync(&page, QUrl("about:blank"));
QCOMPARE(page.isAudioMuted(), true);
QCOMPARE(spy.count(), 1);
QCOMPARE(spy[0][0], QVariant(true));
page.setAudioMuted(false);
QCOMPARE(page.isAudioMuted(), false);
QCOMPARE(spy.count(), 2);
QCOMPARE(spy[1][0], QVariant(false));
}
void tst_QWebEnginePage::closeContents()
{
TestPage page;
QSignalSpy windowCreatedSpy(&page, &TestPage::windowCreated);
page.runJavaScript("var dialog = window.open('', '', 'width=100, height=100');");
QTRY_COMPARE(windowCreatedSpy.count(), 1);
QWebEngineView *dialogView = new QWebEngineView;
QWebEnginePage *dialogPage = page.createdWindows[0];
dialogPage->setView(dialogView);
QCOMPARE(dialogPage->lifecycleState(), QWebEnginePage::LifecycleState::Active);
// This should not crash.
connect(dialogPage, &QWebEnginePage::windowCloseRequested, dialogView, &QWebEngineView::close);
page.runJavaScript("dialog.close();");
// QWebEngineView::closeEvent() sets the life cycle state to discarded.
QTRY_COMPARE(dialogPage->lifecycleState(), QWebEnginePage::LifecycleState::Discarded);
delete dialogView;
}
// Based on QTBUG-84011
void tst_QWebEnginePage::isSafeRedirect_data()
{
QTest::addColumn<QUrl>("requestedUrl");
QTest::addColumn<QUrl>("expectedUrl");
QString fileScheme = "file://";
#ifdef Q_OS_WIN
fileScheme += "/";
#endif
QString tempDir(fileScheme + QDir::tempPath());
QTest::newRow(qPrintable(tempDir)) << QUrl(tempDir) << QUrl(tempDir + "/");
QTest::newRow(qPrintable(tempDir + QString("/foo/bar"))) << QUrl(tempDir + "/foo/bar") << QUrl(tempDir + "/foo/bar");
QTest::newRow("filesystem:http://foo.com/bar") << QUrl("filesystem:http://foo.com/bar") << QUrl("filesystem:http://foo.com/bar/");
}
void tst_QWebEnginePage::isSafeRedirect()
{
QFETCH(QUrl, requestedUrl);
QFETCH(QUrl, expectedUrl);
TestPage page;
QSignalSpy spy(&page, SIGNAL(loadFinished(bool)));
page.setUrl(requestedUrl);
QTRY_COMPARE_WITH_TIMEOUT(spy.count(), 1, 20000);
QCOMPARE(page.url(), expectedUrl);
spy.clear();
}
static QByteArrayList params = {QByteArrayLiteral("--use-fake-device-for-media-stream")};
W_QTEST_MAIN(tst_QWebEnginePage, params)
#include "tst_qwebenginepage.moc"