diff options
Diffstat (limited to 'tests/auto/wasm')
21 files changed, 3888 insertions, 645 deletions
diff --git a/tests/auto/wasm/CMakeLists.txt b/tests/auto/wasm/CMakeLists.txt index 2e6c6976fb..35b4d45e53 100644 --- a/tests/auto/wasm/CMakeLists.txt +++ b/tests/auto/wasm/CMakeLists.txt @@ -1,17 +1,9 @@ -##################################################################### -## tst_wasm Test: -##################################################################### +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause -qt_internal_add_test(tst_qstdweb - SOURCES - tst_qstdweb.cpp - DEFINES - QT_NO_FOREACH - QT_NO_KEYWORDS - LIBRARIES - Qt::GuiPrivate - PUBLIC_LIBRARIES - Qt::Core - Qt::Gui - Qt::Widgets -) +add_subdirectory(fetchapi) +add_subdirectory(localfileapi) +add_subdirectory(qwasmkeytranslator) +add_subdirectory(qwasmwindowstack) +add_subdirectory(qwasmwindowtreenode) +add_subdirectory(selenium) diff --git a/tests/auto/wasm/fetchapi/CMakeLists.txt b/tests/auto/wasm/fetchapi/CMakeLists.txt new file mode 100644 index 0000000000..335eefc1da --- /dev/null +++ b/tests/auto/wasm/fetchapi/CMakeLists.txt @@ -0,0 +1,22 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +##################################################################### +## tst_localfileapi Test: +##################################################################### + +if(NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT) + cmake_minimum_required(VERSION 3.16) + project(tst_fetchapi LANGUAGES CXX) + find_package(Qt6BuildInternals REQUIRED COMPONENTS STANDALONE_TEST) +endif() + +qt_internal_add_test(tst_fetchapi + SOURCES + tst_fetchapi.cpp + DEFINES + QT_NO_FOREACH + LIBRARIES + Qt::Core + Qt::Network +) diff --git a/tests/auto/wasm/fetchapi/tst_fetchapi.cpp b/tests/auto/wasm/fetchapi/tst_fetchapi.cpp new file mode 100644 index 0000000000..3dcd8dd916 --- /dev/null +++ b/tests/auto/wasm/fetchapi/tst_fetchapi.cpp @@ -0,0 +1,86 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// Copyright (C) 2016 Intel Corporation. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include <QTest> +#include <QEventLoop> +#include <QNetworkReply> +#include <QNetworkRequest> +#include <QScopedPointer> + +#include <memory> + +namespace { + +const QUrl URL = QUrl("http://localhost:6931/test_batch.html"); + +class BackgroundThread : public QThread +{ + Q_OBJECT + void run() override + { + manager = std::make_unique<QNetworkAccessManager>(); + eventLoop = std::make_unique<QEventLoop>(); + reply = manager->get(request); + QObject::connect(reply, &QNetworkReply::finished, this, &BackgroundThread::requestFinished); + eventLoop->exec(); + } + + void requestFinished() + { + eventLoop->quit(); + } + +public: + QNetworkReply *reply{ nullptr }; + +private: + std::unique_ptr<QNetworkAccessManager> manager; + std::unique_ptr<QEventLoop> eventLoop; + QNetworkRequest request{ URL }; +}; + +} // namespace + +class tst_FetchApi : public QObject +{ + Q_OBJECT +public: + tst_FetchApi() = default; + +private Q_SLOTS: + void sendRequestOnMainThread(); + void sendRequestOnBackgroundThread(); +}; + +void tst_FetchApi::sendRequestOnMainThread() +{ + QNetworkAccessManager manager; + QNetworkRequest request(URL); + QNetworkReply *reply = manager.get(request); + + QEventLoop loop; + QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + loop.exec(); + + QVERIFY(reply != nullptr); + QVERIFY(reply->error() == QNetworkReply::NoError); +} + +void tst_FetchApi::sendRequestOnBackgroundThread() +{ + QSKIP("Skip this test until we fix fetching from background threads."); + QEventLoop mainEventLoop; + BackgroundThread *backgroundThread = new BackgroundThread(); + connect(backgroundThread, &BackgroundThread::finished, &mainEventLoop, &QEventLoop::quit); + connect(backgroundThread, &BackgroundThread::finished, backgroundThread, &QObject::deleteLater); + backgroundThread->start(); + mainEventLoop.exec(); + + QVERIFY(backgroundThread != nullptr); + QVERIFY(backgroundThread->reply != nullptr); + QVERIFY(backgroundThread->reply->error() == QNetworkReply::NoError); +} + +QTEST_MAIN(tst_FetchApi); +#include "tst_fetchapi.moc" diff --git a/tests/auto/wasm/localfileapi/CMakeLists.txt b/tests/auto/wasm/localfileapi/CMakeLists.txt new file mode 100644 index 0000000000..efb29ad48c --- /dev/null +++ b/tests/auto/wasm/localfileapi/CMakeLists.txt @@ -0,0 +1,24 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +##################################################################### +## tst_localfileapi Test: +##################################################################### + +if(NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT) + cmake_minimum_required(VERSION 3.16) + project(tst_localfileapi LANGUAGES CXX) + find_package(Qt6BuildInternals REQUIRED COMPONENTS STANDALONE_TEST) +endif() + +qt_internal_add_test(tst_localfileapi + SOURCES + tst_localfileapi.cpp + DEFINES + QT_NO_FOREACH + LIBRARIES + Qt::GuiPrivate + Qt::Core + Qt::Gui + Qt::Widgets +) diff --git a/tests/auto/wasm/localfileapi/tst_localfileapi.cpp b/tests/auto/wasm/localfileapi/tst_localfileapi.cpp new file mode 100644 index 0000000000..47e8c06e1e --- /dev/null +++ b/tests/auto/wasm/localfileapi/tst_localfileapi.cpp @@ -0,0 +1,260 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// Copyright (C) 2016 Intel Corporation. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include <QtGui/private/qlocalfileapi_p.h> +#include <QTest> +#include <emscripten/val.h> + +class tst_LocalFileApi : public QObject +{ + Q_OBJECT + +private: + emscripten::val makeAccept(std::vector<emscripten::val> types) + { + auto accept = emscripten::val::object(); + accept.set("application/octet-stream", + emscripten::val::array(std::move(types))); + return accept; + } + + emscripten::val makeType(QString description, std::vector<emscripten::val> acceptExtensions) + { + using namespace emscripten; + + auto type = val::object(); + type.set("description", description.toStdString()); + + auto accept = val::object(); + accept.set("application/octet-stream", + val::array(std::move(acceptExtensions))); + type.set("accept", makeAccept(std::move(acceptExtensions))); + + return type; + } + + emscripten::val makeOpenFileOptions(bool acceptMultiple, std::vector<emscripten::val> types) { + using namespace emscripten; + + auto webFilter = val::object(); + webFilter.set("types", val::array(std::move(types))); + if (!types.empty()) + webFilter.set("excludeAcceptAllOption", val(true)); + webFilter.set("multiple", val(acceptMultiple)); + + return webFilter; + } + + emscripten::val makeSaveFileOptions(QString suggestedName, std::vector<emscripten::val> types) { + using namespace emscripten; + + auto webFilter = val::object(); + webFilter.set("suggestedName", val(suggestedName.toStdString())); + webFilter.set("types", val::array(std::move(types))); + + return webFilter; + } + +private Q_SLOTS: + void fileExtensionFilterTransformation_data(); + void fileExtensionFilterTransformation(); + void acceptTransformation_data(); + void acceptTransformation(); + void typeTransformation_data(); + void typeTransformation(); + void openFileOptions_data(); + void openFileOptions(); + void saveFileOptions_data(); + void saveFileOptions(); + void fileInputAccept_data(); + void fileInputAccept(); +}; + +bool valDeepEquals(emscripten::val lhs, emscripten::val rhs) +{ + auto json = emscripten::val::global("JSON"); + auto lhsAsJsonString = json.call<emscripten::val>("stringify", lhs); + auto rhsAsJsonString = json.call<emscripten::val>("stringify", rhs); + + return lhsAsJsonString.equals(rhsAsJsonString); +} + +void tst_LocalFileApi::fileExtensionFilterTransformation_data() +{ + QTest::addColumn<QString>("qtFileFilter"); + QTest::addColumn<std::optional<QString>>("expectedWebExtensionFilter"); + + QTest::newRow("PNG extension with an asterisk") + << QString("*.png") << std::make_optional<QString>(".png"); + QTest::newRow("Long extension with an asterisk") + << QString("*.someotherfile") << std::make_optional<QString>(".someotherfile"); + QTest::newRow(".dat with no asterisk") + << QString(".dat") << std::make_optional<QString>(".dat"); + QTest::newRow("Multiple asterisks") << QString("*ot*.abc") << std::optional<QString>(); + QTest::newRow("Filename") << QString("abcd.abc") << std::optional<QString>(); + QTest::newRow("match all") << QString("*.*") << std::optional<QString>(); +} + +void tst_LocalFileApi::fileExtensionFilterTransformation() +{ + QFETCH(QString, qtFileFilter); + QFETCH(std::optional<QString>, expectedWebExtensionFilter); + + auto result = LocalFileApi::Type::Accept::MimeType::Extension::fromQt(qtFileFilter); + if (expectedWebExtensionFilter) { + QCOMPARE_EQ(expectedWebExtensionFilter, result->value()); + } else { + QVERIFY(!result.has_value()); + } +} + +void tst_LocalFileApi::acceptTransformation_data() +{ + using namespace emscripten; + + QTest::addColumn<QString>("qtFilterList"); + QTest::addColumn<QStringList>("expectedExtensionList"); + + QTest::newRow("Multiple types") + << QString("*.png *.other *.txt") << QStringList{ ".png", ".other", ".txt" }; + + QTest::newRow("Single type") << QString("*.png") << QStringList{ ".png" }; + + QTest::newRow("No filter when accepts all") << QString("*.*") << QStringList(); + + QTest::newRow("No filter when one filter accepts all") << QString("*.* *.jpg") << QStringList(); + + QTest::newRow("Weird spaces") << QString(" *.jpg *.png *.icon ") + << QStringList{ ".jpg", ".png", ".icon" }; +} + +void tst_LocalFileApi::acceptTransformation() +{ + QFETCH(QString, qtFilterList); + QFETCH(QStringList, expectedExtensionList); + + auto result = LocalFileApi::Type::Accept::fromQt(qtFilterList); + if (expectedExtensionList.isEmpty()) { + QVERIFY(!result.has_value()); + } else { + QStringList transformed; + std::transform(result->mimeType().extensions().begin(), + result->mimeType().extensions().end(), std::back_inserter(transformed), + [](const LocalFileApi::Type::Accept::MimeType::Extension &extension) { + return extension.value().toString(); + }); + QCOMPARE_EQ(expectedExtensionList, transformed); + } +} + +void tst_LocalFileApi::typeTransformation_data() +{ + using namespace emscripten; + + QTest::addColumn<QString>("qtFilterList"); + QTest::addColumn<QString>("expectedDescription"); + QTest::addColumn<QStringList>("expectedExtensions"); + + QTest::newRow("With description") + << QString("Text files (*.txt)") << QString("Text files") << QStringList{ ".txt" }; + + QTest::newRow("No description") << QString("*.jpg") << QString("") << QStringList{ ".jpg" }; +} + +void tst_LocalFileApi::typeTransformation() +{ + QFETCH(QString, qtFilterList); + QFETCH(QString, expectedDescription); + QFETCH(QStringList, expectedExtensions); + + auto result = LocalFileApi::Type::fromQt(qtFilterList); + QCOMPARE_EQ(result->description(), expectedDescription); + + QStringList transformed; + std::transform(result->accept()->mimeType().extensions().begin(), + result->accept()->mimeType().extensions().end(), std::back_inserter(transformed), + [](const LocalFileApi::Type::Accept::MimeType::Extension &extension) { + return extension.value().toString(); + }); + QCOMPARE_EQ(expectedExtensions, transformed); +} + +void tst_LocalFileApi::openFileOptions_data() +{ + using namespace emscripten; + + QTest::addColumn<QStringList>("qtFilterList"); + QTest::addColumn<bool>("multiple"); + QTest::addColumn<emscripten::val>("expectedWebType"); + + QTest::newRow("Multiple files") << QStringList({"Text files (*.txt)", "Images (*.jpg *.png)", "*.bat"}) + << true + << makeOpenFileOptions(true, { makeType("Text files", { val(".txt")}), makeType("Images", { val(".jpg"), val(".png")}), makeType("", { val(".bat")})}); + QTest::newRow("Single file") << QStringList({"Text files (*.txt)", "Images (*.jpg *.png)", "*.bat"}) + << false + << makeOpenFileOptions(false, { makeType("Text files", { val(".txt")}), makeType("Images", { val(".jpg"), val(".png")}), makeType("", { val(".bat")})}); +} + +void tst_LocalFileApi::openFileOptions() +{ + QFETCH(QStringList, qtFilterList); + QFETCH(bool, multiple); + QFETCH(emscripten::val, expectedWebType); + + auto result = LocalFileApi::makeOpenFileOptions(qtFilterList, multiple); + QVERIFY(valDeepEquals(result, expectedWebType)); +} + +void tst_LocalFileApi::saveFileOptions_data() +{ + using namespace emscripten; + + QTest::addColumn<QStringList>("qtFilterList"); + QTest::addColumn<QString>("suggestedName"); + QTest::addColumn<emscripten::val>("expectedWebType"); + + QTest::newRow("Multiple files") << QStringList({"Text files (*.txt)", "Images (*.jpg *.png)", "*.bat"}) + << "someName1" + << makeSaveFileOptions("someName1", { makeType("Text files", { val(".txt")}), makeType("Images", { val(".jpg"), val(".png")}), makeType("", { val(".bat")})}); + QTest::newRow("Single file") << QStringList({"Text files (*.txt)", "Images (*.jpg *.png)", "*.bat"}) + << "some name 2" + << makeSaveFileOptions("some name 2", { makeType("Text files", { val(".txt")}), makeType("Images", { val(".jpg"), val(".png")}), makeType("", { val(".bat")})}); +} + +void tst_LocalFileApi::saveFileOptions() +{ + QFETCH(QStringList, qtFilterList); + QFETCH(QString, suggestedName); + QFETCH(emscripten::val, expectedWebType); + + auto result = LocalFileApi::makeSaveFileOptions(qtFilterList, suggestedName.toStdString()); + QVERIFY(valDeepEquals(result, expectedWebType)); +} + +void tst_LocalFileApi::fileInputAccept_data() +{ + using namespace emscripten; + + QTest::addColumn<QStringList>("qtFilterList"); + QTest::addColumn<QString>("expectedAccept"); + + QTest::newRow("Multiple files") + << QStringList{ "Text files (*.txt)", "Images (*.jpg *.png)", "*.bat" } + << ".txt,.jpg,.png,.bat"; + QTest::newRow("Spaces") << QStringList{ " Documents (*.doc)", "Everything (*.*)", + " Stuff ( *.stf *.tng)", " *.exe" } + << ".doc,.stf,.tng,.exe"; +} + +void tst_LocalFileApi::fileInputAccept() +{ + QFETCH(QStringList, qtFilterList); + QFETCH(QString, expectedAccept); + + auto result = LocalFileApi::makeFileInputAccept(qtFilterList); + QCOMPARE_EQ(expectedAccept, QString::fromStdString(result)); +} + +QTEST_APPLESS_MAIN(tst_LocalFileApi) +#include "tst_localfileapi.moc" diff --git a/tests/auto/wasm/qwasmkeytranslator/CMakeLists.txt b/tests/auto/wasm/qwasmkeytranslator/CMakeLists.txt new file mode 100644 index 0000000000..b999dbe028 --- /dev/null +++ b/tests/auto/wasm/qwasmkeytranslator/CMakeLists.txt @@ -0,0 +1,24 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +##################################################################### +## tst_qwasmkeytranslator Test: +##################################################################### + +if(NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT) + cmake_minimum_required(VERSION 3.16) + project(tst_qwasmkeytranslator LANGUAGES CXX) + find_package(Qt6BuildInternals REQUIRED COMPONENTS STANDALONE_TEST) +endif() + +qt_internal_add_test(tst_qwasmkeytranslator + SOURCES + tst_qwasmkeytranslator.cpp + DEFINES + QT_NO_FOREACH + LIBRARIES + Qt::Gui + Qt::GuiPrivate + PUBLIC_LIBRARIES + Qt::Core +) diff --git a/tests/auto/wasm/qwasmkeytranslator/tst_qwasmkeytranslator.cpp b/tests/auto/wasm/qwasmkeytranslator/tst_qwasmkeytranslator.cpp new file mode 100644 index 0000000000..de06c298df --- /dev/null +++ b/tests/auto/wasm/qwasmkeytranslator/tst_qwasmkeytranslator.cpp @@ -0,0 +1,425 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "../../../../src/plugins/platforms/wasm/qwasmkeytranslator.h" + +#include "../../../../src/plugins/platforms/wasm/qwasmevent.h" + +#include <QTest> + +#include <emscripten/val.h> + +namespace { +emscripten::val makeDeadKeyJsEvent(QString code, Qt::KeyboardModifiers modifiers) +{ + auto jsEvent = emscripten::val::object(); + jsEvent.set("code", emscripten::val(code.toStdString())); + jsEvent.set("key", emscripten::val("Dead")); + jsEvent.set("shiftKey", emscripten::val(modifiers.testFlag(Qt::ShiftModifier))); + jsEvent.set("ctrlKey", emscripten::val(false)); + jsEvent.set("altKey", emscripten::val(false)); + jsEvent.set("metaKey", emscripten::val(false)); + + return jsEvent; +} + +emscripten::val makeKeyJsEvent(QString code, QString key, Qt::KeyboardModifiers modifiers) +{ + auto jsEvent = emscripten::val::object(); + jsEvent.set("code", emscripten::val(code.toStdString())); + jsEvent.set("key", emscripten::val(key.toStdString())); + jsEvent.set("shiftKey", emscripten::val(modifiers.testFlag(Qt::ShiftModifier))); + jsEvent.set("ctrlKey", emscripten::val(modifiers.testFlag(Qt::ControlModifier))); + jsEvent.set("altKey", emscripten::val(modifiers.testFlag(Qt::AltModifier))); + jsEvent.set("metaKey", emscripten::val(modifiers.testFlag(Qt::MetaModifier))); + + return jsEvent; +} +} // unnamed namespace + +class tst_QWasmKeyTranslator : public QObject +{ + Q_OBJECT + +public: + tst_QWasmKeyTranslator() = default; + +private slots: + void init(); + + void modifyByDeadKey_data(); + void modifyByDeadKey(); + void deadKeyModifiesOnlyOneKeyPressAndUp(); + void deadKeyIgnoresKeyUpPrecedingKeyDown(); + void onlyKeysProducingTextAreModifiedByDeadKeys(); +}; + +void tst_QWasmKeyTranslator::init() { } + +void tst_QWasmKeyTranslator::modifyByDeadKey_data() +{ + QTest::addColumn<QString>("deadKeyCode"); + QTest::addColumn<Qt::KeyboardModifiers>("deadKeyModifiers"); + QTest::addColumn<QString>("targetKeyCode"); + QTest::addColumn<QString>("targetKey"); + QTest::addColumn<Qt::Key>("targetQtKey"); + QTest::addColumn<Qt::KeyboardModifiers>("modifiers"); + QTest::addColumn<QString>("expectedModifiedKey"); + + QTest::newRow("à (Backquote)") << "Backquote" << Qt::KeyboardModifiers() << "KeyA" + << "a" << Qt::Key_Agrave << Qt::KeyboardModifiers() << "à"; + QTest::newRow("À (Backquote)") + << "Backquote" << Qt::KeyboardModifiers() << "KeyA" + << "A" << Qt::Key_Agrave << Qt::KeyboardModifiers(Qt::ShiftModifier) << "À"; + QTest::newRow("à (IntlBackslash)") << "IntlBackslash" << Qt::KeyboardModifiers() << "KeyA" + << "a" << Qt::Key_Agrave << Qt::KeyboardModifiers() << "à"; + QTest::newRow("À (IntlBackslash)") + << "IntlBackslash" << Qt::KeyboardModifiers() << "KeyA" + << "A" << Qt::Key_Agrave << Qt::KeyboardModifiers(Qt::ShiftModifier) << "À"; + QTest::newRow("á (Quote)") << "Quote" << Qt::KeyboardModifiers() << "KeyA" + << "a" << Qt::Key_Aacute << Qt::KeyboardModifiers() << "á"; + QTest::newRow("Á (Quote)") << "Quote" << Qt::KeyboardModifiers() << "KeyA" + << "A" << Qt::Key_Aacute << Qt::KeyboardModifiers(Qt::ShiftModifier) + << "Á"; + QTest::newRow("á") << "KeyE" << Qt::KeyboardModifiers() << "KeyA" + << "a" << Qt::Key_Aacute << Qt::KeyboardModifiers() << "á"; + QTest::newRow("Á") << "KeyE" << Qt::KeyboardModifiers() << "KeyA" + << "A" << Qt::Key_Aacute << Qt::KeyboardModifiers(Qt::ShiftModifier) << "Á"; + QTest::newRow("ä (Mac Umlaut)") << "KeyU" << Qt::KeyboardModifiers() << "KeyA" + << "a" << Qt::Key_Adiaeresis << Qt::KeyboardModifiers() << "ä"; + QTest::newRow("Ä (Mac Umlaut)") + << "KeyU" << Qt::KeyboardModifiers() << "KeyA" + << "A" << Qt::Key_Adiaeresis << Qt::KeyboardModifiers(Qt::ShiftModifier) << "Ä"; + QTest::newRow("ä (Shift+Quote)") + << "Quote" << Qt::KeyboardModifiers(Qt::ShiftModifier) << "KeyA" + << "a" << Qt::Key_Adiaeresis << Qt::KeyboardModifiers() << "ä"; + QTest::newRow("Ä (Shift+Quote)") + << "Quote" << Qt::KeyboardModifiers(Qt::ShiftModifier) << "KeyA" + << "A" << Qt::Key_Adiaeresis << Qt::KeyboardModifiers(Qt::ShiftModifier) << "Ä"; + QTest::newRow("â") << "KeyI" << Qt::KeyboardModifiers() << "KeyA" + << "a" << Qt::Key_Acircumflex << Qt::KeyboardModifiers() << "â"; + QTest::newRow("Â") << "KeyI" << Qt::KeyboardModifiers() << "KeyA" + << "A" << Qt::Key_Acircumflex << Qt::KeyboardModifiers(Qt::ShiftModifier) + << "Â"; + QTest::newRow("â (^ key)") << "Digit6" << Qt::KeyboardModifiers(Qt::ShiftModifier) << "KeyA" + << "a" << Qt::Key_Acircumflex << Qt::KeyboardModifiers() << "â"; + QTest::newRow("Â (^ key)") << "Digit6" << Qt::KeyboardModifiers(Qt::ShiftModifier) << "KeyA" + << "A" << Qt::Key_Acircumflex + << Qt::KeyboardModifiers(Qt::ShiftModifier) << "Â"; + QTest::newRow("ã") << "KeyN" << Qt::KeyboardModifiers() << "KeyA" + << "a" << Qt::Key_Atilde << Qt::KeyboardModifiers() << "ã"; + QTest::newRow("Ã") << "KeyN" << Qt::KeyboardModifiers() << "KeyA" + << "A" << Qt::Key_Atilde << Qt::KeyboardModifiers(Qt::ShiftModifier) << "Ã"; + + QTest::newRow("è (Backquote)") << "Backquote" << Qt::KeyboardModifiers() << "KeyE" + << "e" << Qt::Key_Egrave << Qt::KeyboardModifiers() << "è"; + QTest::newRow("È (Backquote)") + << "Backquote" << Qt::KeyboardModifiers() << "KeyE" + << "E" << Qt::Key_Egrave << Qt::KeyboardModifiers(Qt::ShiftModifier) << "È"; + QTest::newRow("è") << "IntlBackslash" << Qt::KeyboardModifiers() << "KeyE" + << "e" << Qt::Key_Egrave << Qt::KeyboardModifiers() << "è"; + QTest::newRow("È") << "IntlBackslash" << Qt::KeyboardModifiers() << "KeyE" + << "E" << Qt::Key_Egrave << Qt::KeyboardModifiers(Qt::ShiftModifier) << "È"; + QTest::newRow("é") << "KeyE" << Qt::KeyboardModifiers() << "KeyE" + << "e" << Qt::Key_Eacute << Qt::KeyboardModifiers() << "é"; + QTest::newRow("É") << "KeyE" << Qt::KeyboardModifiers() << "KeyE" + << "E" << Qt::Key_Eacute << Qt::KeyboardModifiers(Qt::ShiftModifier) << "É"; + QTest::newRow("é (Quote)") << "Quote" << Qt::KeyboardModifiers() << "KeyE" + << "e" << Qt::Key_Eacute << Qt::KeyboardModifiers() << "é"; + QTest::newRow("É (Quote)") << "Quote" << Qt::KeyboardModifiers() << "KeyE" + << "E" << Qt::Key_Eacute << Qt::KeyboardModifiers(Qt::ShiftModifier) + << "É"; + QTest::newRow("ë (Mac Umlaut)") << "KeyU" << Qt::KeyboardModifiers() << "KeyE" + << "e" << Qt::Key_Ediaeresis << Qt::KeyboardModifiers() << "ë"; + QTest::newRow("Ë (Mac Umlaut)") + << "KeyU" << Qt::KeyboardModifiers() << "KeyE" + << "E" << Qt::Key_Ediaeresis << Qt::KeyboardModifiers(Qt::ShiftModifier) << "Ë"; + QTest::newRow("ë (Shift+Quote)") + << "Quote" << Qt::KeyboardModifiers(Qt::ShiftModifier) << "KeyE" + << "e" << Qt::Key_Ediaeresis << Qt::KeyboardModifiers() << "ë"; + QTest::newRow("Ë (Shift+Quote)") + << "Quote" << Qt::KeyboardModifiers(Qt::ShiftModifier) << "KeyE" + << "E" << Qt::Key_Ediaeresis << Qt::KeyboardModifiers(Qt::ShiftModifier) << "Ë"; + QTest::newRow("ê") << "KeyI" << Qt::KeyboardModifiers() << "KeyE" + << "e" << Qt::Key_Ecircumflex << Qt::KeyboardModifiers() << "ê"; + QTest::newRow("Ê") << "KeyI" << Qt::KeyboardModifiers() << "KeyE" + << "E" << Qt::Key_Ecircumflex << Qt::KeyboardModifiers(Qt::ShiftModifier) + << "Ê"; + QTest::newRow("ê (^ key)") << "Digit6" << Qt::KeyboardModifiers(Qt::ShiftModifier) << "KeyE" + << "e" << Qt::Key_Ecircumflex << Qt::KeyboardModifiers() << "ê"; + QTest::newRow("Ê (^ key)") << "Digit6" << Qt::KeyboardModifiers(Qt::ShiftModifier) << "KeyE" + << "E" << Qt::Key_Ecircumflex + << Qt::KeyboardModifiers(Qt::ShiftModifier) << "Ê"; + + QTest::newRow("ì (Backquote)") << "Backquote" << Qt::KeyboardModifiers() << "KeyI" + << "i" << Qt::Key_Igrave << Qt::KeyboardModifiers() << "ì"; + QTest::newRow("Ì (Backquote)") + << "Backquote" << Qt::KeyboardModifiers() << "KeyI" + << "I" << Qt::Key_Igrave << Qt::KeyboardModifiers(Qt::ShiftModifier) << "Ì"; + QTest::newRow("ì") << "IntlBackslash" << Qt::KeyboardModifiers() << "KeyI" + << "i" << Qt::Key_Igrave << Qt::KeyboardModifiers() << "ì"; + QTest::newRow("Ì") << "IntlBackslash" << Qt::KeyboardModifiers() << "KeyI" + << "I" << Qt::Key_Igrave << Qt::KeyboardModifiers(Qt::ShiftModifier) << "Ì"; + QTest::newRow("í") << "KeyE" << Qt::KeyboardModifiers() << "KeyI" + << "i" << Qt::Key_Iacute << Qt::KeyboardModifiers() << "í"; + QTest::newRow("Í") << "KeyE" << Qt::KeyboardModifiers() << "KeyI" + << "I" << Qt::Key_Iacute << Qt::KeyboardModifiers(Qt::ShiftModifier) << "Í"; + QTest::newRow("í (Quote)") << "Quote" << Qt::KeyboardModifiers() << "KeyI" + << "i" << Qt::Key_Iacute << Qt::KeyboardModifiers() << "í"; + QTest::newRow("Í (Quote)") << "Quote" << Qt::KeyboardModifiers() << "KeyI" + << "I" << Qt::Key_Iacute << Qt::KeyboardModifiers(Qt::ShiftModifier) + << "Í"; + QTest::newRow("ï (Mac Umlaut)") << "KeyU" << Qt::KeyboardModifiers() << "KeyI" + << "i" << Qt::Key_Idiaeresis << Qt::KeyboardModifiers() << "ï"; + QTest::newRow("Ï (Mac Umlaut)") + << "KeyU" << Qt::KeyboardModifiers() << "KeyI" + << "I" << Qt::Key_Idiaeresis << Qt::KeyboardModifiers(Qt::ShiftModifier) << "Ï"; + QTest::newRow("ï (Shift+Quote)") + << "Quote" << Qt::KeyboardModifiers(Qt::ShiftModifier) << "KeyI" + << "i" << Qt::Key_Idiaeresis << Qt::KeyboardModifiers() << "ï"; + QTest::newRow("Ï (Shift+Quote)") + << "Quote" << Qt::KeyboardModifiers(Qt::ShiftModifier) << "KeyI" + << "I" << Qt::Key_Idiaeresis << Qt::KeyboardModifiers(Qt::ShiftModifier) << "Ï"; + QTest::newRow("î") << "KeyI" << Qt::KeyboardModifiers() << "KeyI" + << "i" << Qt::Key_Icircumflex << Qt::KeyboardModifiers() << "î"; + QTest::newRow("Î") << "KeyI" << Qt::KeyboardModifiers() << "KeyI" + << "I" << Qt::Key_Icircumflex << Qt::KeyboardModifiers(Qt::ShiftModifier) + << "Î"; + QTest::newRow("î (^ key)") << "Digit6" << Qt::KeyboardModifiers() << "KeyI" + << "i" << Qt::Key_Icircumflex << Qt::KeyboardModifiers() << "î"; + QTest::newRow("Î (^ key)") << "Digit6" << Qt::KeyboardModifiers() << "KeyI" + << "I" << Qt::Key_Icircumflex + << Qt::KeyboardModifiers(Qt::ShiftModifier) << "Î"; + + QTest::newRow("ñ") << "KeyN" << Qt::KeyboardModifiers() << "KeyN" + << "n" << Qt::Key_Ntilde << Qt::KeyboardModifiers() << "ñ"; + QTest::newRow("Ñ") << "KeyN" << Qt::KeyboardModifiers() << "KeyN" + << "N" << Qt::Key_Ntilde << Qt::KeyboardModifiers(Qt::ShiftModifier) << "Ñ"; + + QTest::newRow("ò (Backquote)") << "Backquote" << Qt::KeyboardModifiers() << "KeyO" + << "o" << Qt::Key_Ograve << Qt::KeyboardModifiers() << "ò"; + QTest::newRow("Ò (Backquote)") + << "Backquote" << Qt::KeyboardModifiers() << "KeyO" + << "O" << Qt::Key_Ograve << Qt::KeyboardModifiers(Qt::ShiftModifier) << "Ò"; + QTest::newRow("ò") << "IntlBackslash" << Qt::KeyboardModifiers() << "KeyO" + << "o" << Qt::Key_Ograve << Qt::KeyboardModifiers() << "ò"; + QTest::newRow("Ò") << "IntlBackslash" << Qt::KeyboardModifiers() << "KeyO" + << "O" << Qt::Key_Ograve << Qt::KeyboardModifiers(Qt::ShiftModifier) << "Ò"; + QTest::newRow("ó") << "KeyE" << Qt::KeyboardModifiers() << "KeyO" + << "o" << Qt::Key_Oacute << Qt::KeyboardModifiers() << "ó"; + QTest::newRow("Ó") << "KeyE" << Qt::KeyboardModifiers() << "KeyO" + << "O" << Qt::Key_Oacute << Qt::KeyboardModifiers(Qt::ShiftModifier) << "Ó"; + QTest::newRow("ó (Quote)") << "Quote" << Qt::KeyboardModifiers() << "KeyO" + << "o" << Qt::Key_Oacute << Qt::KeyboardModifiers() << "ó"; + QTest::newRow("Ó (Quote)") << "Quote" << Qt::KeyboardModifiers() << "KeyO" + << "O" << Qt::Key_Oacute << Qt::KeyboardModifiers(Qt::ShiftModifier) + << "Ó"; + QTest::newRow("ö (Mac Umlaut)") << "KeyU" << Qt::KeyboardModifiers() << "KeyO" + << "o" << Qt::Key_Odiaeresis << Qt::KeyboardModifiers() << "ö"; + QTest::newRow("Ö (Mac Umlaut)") + << "KeyU" << Qt::KeyboardModifiers() << "KeyO" + << "O" << Qt::Key_Odiaeresis << Qt::KeyboardModifiers(Qt::ShiftModifier) << "Ö"; + QTest::newRow("ö (Shift+Quote)") + << "Quote" << Qt::KeyboardModifiers(Qt::ShiftModifier) << "KeyO" + << "o" << Qt::Key_Odiaeresis << Qt::KeyboardModifiers() << "ö"; + QTest::newRow("Ö (Shift+Quote)") + << "Quote" << Qt::KeyboardModifiers(Qt::ShiftModifier) << "KeyO" + << "O" << Qt::Key_Odiaeresis << Qt::KeyboardModifiers(Qt::ShiftModifier) << "Ö"; + QTest::newRow("ô") << "KeyI" << Qt::KeyboardModifiers() << "KeyO" + << "o" << Qt::Key_Ocircumflex << Qt::KeyboardModifiers() << "ô"; + QTest::newRow("Ô") << "KeyI" << Qt::KeyboardModifiers() << "KeyO" + << "O" << Qt::Key_Ocircumflex << Qt::KeyboardModifiers(Qt::ShiftModifier) + << "Ô"; + QTest::newRow("ô (^ key)") << "Digit6" << Qt::KeyboardModifiers(Qt::ShiftModifier) << "KeyO" + << "o" << Qt::Key_Ocircumflex << Qt::KeyboardModifiers() << "ô"; + QTest::newRow("Ô (^ key)") << "Digit6" << Qt::KeyboardModifiers(Qt::ShiftModifier) << "KeyO" + << "O" << Qt::Key_Ocircumflex + << Qt::KeyboardModifiers(Qt::ShiftModifier) << "Ô"; + QTest::newRow("õ") << "KeyN" << Qt::KeyboardModifiers() << "KeyO" + << "o" << Qt::Key_Otilde << Qt::KeyboardModifiers() << "õ"; + QTest::newRow("Õ") << "KeyN" << Qt::KeyboardModifiers() << "KeyO" + << "O" << Qt::Key_Otilde << Qt::KeyboardModifiers(Qt::ShiftModifier) << "Õ"; + + QTest::newRow("ù (Backquote)") << "Backquote" << Qt::KeyboardModifiers() << "KeyU" + << "u" << Qt::Key_Ugrave << Qt::KeyboardModifiers() << "ù"; + QTest::newRow("Ù (Backquote)") + << "Backquote" << Qt::KeyboardModifiers() << "KeyU" + << "U" << Qt::Key_Ugrave << Qt::KeyboardModifiers(Qt::ShiftModifier) << "Ù"; + QTest::newRow("ù") << "IntlBackslash" << Qt::KeyboardModifiers() << "KeyU" + << "u" << Qt::Key_Ugrave << Qt::KeyboardModifiers() << "ù"; + QTest::newRow("Ù") << "IntlBackslash" << Qt::KeyboardModifiers() << "KeyU" + << "U" << Qt::Key_Ugrave << Qt::KeyboardModifiers(Qt::ShiftModifier) << "Ù"; + QTest::newRow("ú") << "KeyE" << Qt::KeyboardModifiers() << "KeyU" + << "u" << Qt::Key_Uacute << Qt::KeyboardModifiers() << "ú"; + QTest::newRow("Ú") << "KeyE" << Qt::KeyboardModifiers() << "KeyU" + << "U" << Qt::Key_Uacute << Qt::KeyboardModifiers(Qt::ShiftModifier) << "Ú"; + QTest::newRow("ú (Quote)") << "Quote" << Qt::KeyboardModifiers() << "KeyU" + << "u" << Qt::Key_Uacute << Qt::KeyboardModifiers() << "ú"; + QTest::newRow("Ú (Quote)") << "Quote" << Qt::KeyboardModifiers() << "KeyU" + << "U" << Qt::Key_Uacute << Qt::KeyboardModifiers(Qt::ShiftModifier) + << "Ú"; + QTest::newRow("ü (Mac Umlaut)") << "KeyU" << Qt::KeyboardModifiers() << "KeyU" + << "u" << Qt::Key_Udiaeresis << Qt::KeyboardModifiers() << "ü"; + QTest::newRow("Ü (Mac Umlaut)") + << "KeyU" << Qt::KeyboardModifiers() << "KeyU" + << "U" << Qt::Key_Udiaeresis << Qt::KeyboardModifiers(Qt::ShiftModifier) << "Ü"; + QTest::newRow("ü (Shift+Quote)") + << "Quote" << Qt::KeyboardModifiers(Qt::ShiftModifier) << "KeyU" + << "u" << Qt::Key_Udiaeresis << Qt::KeyboardModifiers() << "ü"; + QTest::newRow("Ü (Shift+Quote)") + << "Quote" << Qt::KeyboardModifiers(Qt::ShiftModifier) << "KeyU" + << "U" << Qt::Key_Udiaeresis << Qt::KeyboardModifiers(Qt::ShiftModifier) << "Ü"; + QTest::newRow("û") << "KeyI" << Qt::KeyboardModifiers() << "KeyU" + << "û" << Qt::Key_Ucircumflex << Qt::KeyboardModifiers() << "û"; + QTest::newRow("Û") << "KeyI" << Qt::KeyboardModifiers() << "KeyU" + << "U" << Qt::Key_Ucircumflex << Qt::KeyboardModifiers(Qt::ShiftModifier) + << "Û"; + QTest::newRow("û (^ key)") << "Digit6" << Qt::KeyboardModifiers(Qt::ShiftModifier) << "KeyU" + << "û" << Qt::Key_Ucircumflex << Qt::KeyboardModifiers() << "û"; + QTest::newRow("Û (^ key)") << "Digit6" << Qt::KeyboardModifiers(Qt::ShiftModifier) << "KeyU" + << "U" << Qt::Key_Ucircumflex + << Qt::KeyboardModifiers(Qt::ShiftModifier) << "Û"; + + QTest::newRow("ý") << "KeyE" << Qt::KeyboardModifiers() << "KeyY" + << "y" << Qt::Key_Yacute << Qt::KeyboardModifiers() << "ý"; + QTest::newRow("Ý") << "KeyE" << Qt::KeyboardModifiers() << "KeyY" + << "Y" << Qt::Key_Yacute << Qt::KeyboardModifiers(Qt::ShiftModifier) << "Ý"; + QTest::newRow("ý (Quote)") << "Quote" << Qt::KeyboardModifiers() << "KeyY" + << "y" << Qt::Key_Yacute << Qt::KeyboardModifiers() << "ý"; + QTest::newRow("Ý (Quote)") << "Quote" << Qt::KeyboardModifiers() << "KeyY" + << "Y" << Qt::Key_Yacute << Qt::KeyboardModifiers(Qt::ShiftModifier) + << "Ý"; + QTest::newRow("ÿ (Mac Umlaut)") << "KeyU" << Qt::KeyboardModifiers() << "KeyY" + << "y" << Qt::Key_ydiaeresis << Qt::KeyboardModifiers() << "ÿ"; + QTest::newRow("Ÿ (Mac Umlaut)") + << "KeyU" << Qt::KeyboardModifiers() << "KeyY" + << "Y" << Qt::Key_ydiaeresis << Qt::KeyboardModifiers(Qt::ShiftModifier) << "Ÿ"; + QTest::newRow("ÿ (Shift+Quote)") + << "Quote" << Qt::KeyboardModifiers(Qt::ShiftModifier) << "KeyY" + << "y" << Qt::Key_ydiaeresis << Qt::KeyboardModifiers() << "ÿ"; + QTest::newRow("Ÿ (Shift+Quote)") + << "Quote" << Qt::KeyboardModifiers(Qt::ShiftModifier) << "KeyY" + << "Y" << Qt::Key_ydiaeresis << Qt::KeyboardModifiers(Qt::ShiftModifier) << "Ÿ"; +} + +void tst_QWasmKeyTranslator::modifyByDeadKey() +{ + QFETCH(QString, deadKeyCode); + QFETCH(Qt::KeyboardModifiers, deadKeyModifiers); + QFETCH(QString, targetKeyCode); + QFETCH(QString, targetKey); + QFETCH(Qt::Key, targetQtKey); + QFETCH(Qt::KeyboardModifiers, modifiers); + QFETCH(QString, expectedModifiedKey); + + QWasmDeadKeySupport deadKeySupport; + + KeyEvent event(EventType::KeyDown, makeDeadKeyJsEvent(deadKeyCode, deadKeyModifiers)); + QCOMPARE(event.deadKey, true); + + deadKeySupport.applyDeadKeyTranslations(&event); + + KeyEvent eDown(EventType::KeyDown, makeKeyJsEvent(targetKeyCode, targetKey, modifiers)); + QCOMPARE(eDown.deadKey, false); + deadKeySupport.applyDeadKeyTranslations(&eDown); + QCOMPARE(eDown.deadKey, false); + QCOMPARE(eDown.text, expectedModifiedKey); + QCOMPARE(eDown.key, targetQtKey); + + KeyEvent eUp(EventType::KeyUp, makeKeyJsEvent(targetKeyCode, targetKey, modifiers)); + QCOMPARE(eUp.deadKey, false); + deadKeySupport.applyDeadKeyTranslations(&eUp); + QCOMPARE(eUp.text, expectedModifiedKey); + QCOMPARE(eUp.key, targetQtKey); +} + +void tst_QWasmKeyTranslator::deadKeyModifiesOnlyOneKeyPressAndUp() +{ + QWasmDeadKeySupport deadKeySupport; + KeyEvent event(EventType::KeyDown, makeDeadKeyJsEvent("KeyU", Qt::KeyboardModifiers())); + deadKeySupport.applyDeadKeyTranslations(&event); + + KeyEvent eDown(EventType::KeyDown, makeKeyJsEvent("KeyU", "u", Qt::KeyboardModifiers())); + deadKeySupport.applyDeadKeyTranslations(&eDown); + QCOMPARE(eDown.text, "ü"); + QCOMPARE(eDown.key, Qt::Key_Udiaeresis); + + KeyEvent eUp(EventType::KeyUp, makeKeyJsEvent("KeyU", "u", Qt::KeyboardModifiers())); + deadKeySupport.applyDeadKeyTranslations(&eUp); + QCOMPARE(eUp.text, "ü"); + QCOMPARE(eUp.key, Qt::Key_Udiaeresis); + + KeyEvent eDown2(EventType::KeyDown, makeKeyJsEvent("KeyU", "u", Qt::KeyboardModifiers())); + deadKeySupport.applyDeadKeyTranslations(&eDown2); + QCOMPARE(eDown2.text, "u"); + QCOMPARE(eDown2.key, Qt::Key_U); + + KeyEvent eUp2(EventType::KeyUp, makeKeyJsEvent("KeyU", "u", Qt::KeyboardModifiers())); + deadKeySupport.applyDeadKeyTranslations(&eUp2); + QCOMPARE(eUp2.text, "u"); + QCOMPARE(eUp2.key, Qt::Key_U); +} + +void tst_QWasmKeyTranslator::deadKeyIgnoresKeyUpPrecedingKeyDown() +{ + QWasmDeadKeySupport deadKeySupport; + + KeyEvent deadKeyDownEvent(EventType::KeyDown, + makeDeadKeyJsEvent("KeyU", Qt::KeyboardModifiers())); + deadKeySupport.applyDeadKeyTranslations(&deadKeyDownEvent); + + KeyEvent deadKeyUpEvent(EventType::KeyUp, makeDeadKeyJsEvent("KeyU", Qt::KeyboardModifiers())); + deadKeySupport.applyDeadKeyTranslations(&deadKeyUpEvent); + + KeyEvent otherKeyUpEvent(EventType::KeyUp, + makeKeyJsEvent("AltLeft", "Alt", Qt::KeyboardModifiers())); + deadKeySupport.applyDeadKeyTranslations(&otherKeyUpEvent); + + KeyEvent eDown(EventType::KeyDown, makeKeyJsEvent("KeyU", "u", Qt::KeyboardModifiers())); + deadKeySupport.applyDeadKeyTranslations(&eDown); + QCOMPARE(eDown.text, "ü"); + QCOMPARE(eDown.key, Qt::Key_Udiaeresis); + + KeyEvent yetAnotherKeyUpEvent( + EventType::KeyUp, makeKeyJsEvent("ControlLeft", "Control", Qt::KeyboardModifiers())); + deadKeySupport.applyDeadKeyTranslations(&yetAnotherKeyUpEvent); + + KeyEvent eUp(EventType::KeyUp, makeKeyJsEvent("KeyU", "u", Qt::KeyboardModifiers())); + deadKeySupport.applyDeadKeyTranslations(&eUp); + QCOMPARE(eUp.text, "ü"); + QCOMPARE(eUp.key, Qt::Key_Udiaeresis); +} + +void tst_QWasmKeyTranslator::onlyKeysProducingTextAreModifiedByDeadKeys() +{ + QWasmDeadKeySupport deadKeySupport; + + KeyEvent deadKeyDownEvent(EventType::KeyDown, + makeDeadKeyJsEvent("KeyU", Qt::KeyboardModifiers())); + deadKeySupport.applyDeadKeyTranslations(&deadKeyDownEvent); + + KeyEvent noTextKeyDown(EventType::KeyDown, + makeKeyJsEvent("AltLeft", "Alt", Qt::KeyboardModifiers())); + deadKeySupport.applyDeadKeyTranslations(&noTextKeyDown); + QCOMPARE(noTextKeyDown.text, ""); + QCOMPARE(noTextKeyDown.key, Qt::Key_Alt); + + KeyEvent noTextKeyUp(EventType::KeyUp, + makeKeyJsEvent("AltLeft", "Alt", Qt::KeyboardModifiers())); + deadKeySupport.applyDeadKeyTranslations(&noTextKeyUp); + QCOMPARE(noTextKeyDown.text, ""); + QCOMPARE(noTextKeyDown.key, Qt::Key_Alt); + + KeyEvent eDown(EventType::KeyDown, makeKeyJsEvent("KeyU", "u", Qt::KeyboardModifiers())); + deadKeySupport.applyDeadKeyTranslations(&eDown); + QCOMPARE(eDown.text, "ü"); + QCOMPARE(eDown.key, Qt::Key_Udiaeresis); + + KeyEvent eUp(EventType::KeyUp, makeKeyJsEvent("KeyU", "u", Qt::KeyboardModifiers())); + deadKeySupport.applyDeadKeyTranslations(&eUp); + QCOMPARE(eUp.text, "ü"); + QCOMPARE(eUp.key, Qt::Key_Udiaeresis); +} + +QTEST_MAIN(tst_QWasmKeyTranslator) +#include "tst_qwasmkeytranslator.moc" diff --git a/tests/auto/wasm/qwasmwindowstack/CMakeLists.txt b/tests/auto/wasm/qwasmwindowstack/CMakeLists.txt new file mode 100644 index 0000000000..4b22d8a2eb --- /dev/null +++ b/tests/auto/wasm/qwasmwindowstack/CMakeLists.txt @@ -0,0 +1,25 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +##################################################################### +## tst_qwasmwindowstack Test: +##################################################################### + +if(NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT) + cmake_minimum_required(VERSION 3.16) + project(tst_qwasmwindowstack LANGUAGES CXX) + find_package(Qt6BuildInternals REQUIRED COMPONENTS STANDALONE_TEST) +endif() + +qt_internal_add_test(tst_qwasmwindowstack + SOURCES + tst_qwasmwindowstack.cpp + DEFINES + QT_NO_FOREACH + LIBRARIES + Qt::GuiPrivate + PUBLIC_LIBRARIES + Qt::Core + Qt::Gui + Qt::Widgets +) diff --git a/tests/auto/wasm/qwasmwindowstack/tst_qwasmwindowstack.cpp b/tests/auto/wasm/qwasmwindowstack/tst_qwasmwindowstack.cpp new file mode 100644 index 0000000000..fe169b52dc --- /dev/null +++ b/tests/auto/wasm/qwasmwindowstack/tst_qwasmwindowstack.cpp @@ -0,0 +1,714 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "../../../../src/plugins/platforms/wasm/qwasmwindowstack.h" +#include <QtGui/QWindow> +#include <QTest> +#include <emscripten/val.h> + +class QWasmWindow +{ +}; + +namespace { +std::vector<QWasmWindow *> getWindowsFrontToBack(const QWasmWindowStack *stack) +{ + return std::vector<QWasmWindow *>(stack->begin(), stack->end()); +} +} + +class tst_QWasmWindowStack : public QObject +{ + Q_OBJECT + +public: + tst_QWasmWindowStack() + : m_mockCallback(std::bind(&tst_QWasmWindowStack::onTopWindowChanged, this)) + { + } + +private slots: + void init(); + + void insertion(); + void raising(); + void raisingWithAlwaysOnBottom(); + void raisingWithAlwaysOnTop(); + void lowering(); + void loweringWithAlwaysOnBottom(); + void loweringWithAlwaysOnTop(); + void removing(); + void removingWithAlwaysOnBottom(); + void removingWithAlwaysOnTop(); + void positionPreferenceChanges(); + void clearing(); + +private: + void onTopWindowChanged() + { + ++m_topLevelChangedCallCount; + if (m_onTopLevelChangedAction) + m_onTopLevelChangedAction(); + } + + void verifyWindowOrderChanged(int expected = 1) + { + QCOMPARE(expected, m_topLevelChangedCallCount); + clearCallbackCounter(); + } + + void clearCallbackCounter() { m_topLevelChangedCallCount = 0; } + + QWasmWindowStack::WindowOrderChangedCallbackType m_mockCallback; + QWasmWindowStack::WindowOrderChangedCallbackType m_onTopLevelChangedAction; + int m_topLevelChangedCallCount = 0; + + QWasmWindow m_root; + QWasmWindow m_window1; + QWasmWindow m_window2; + QWasmWindow m_window3; + QWasmWindow m_window4; + QWasmWindow m_window5; +}; + +void tst_QWasmWindowStack::init() +{ + m_onTopLevelChangedAction = QWasmWindowStack::WindowOrderChangedCallbackType(); + clearCallbackCounter(); +} + +void tst_QWasmWindowStack::insertion() +{ + QWasmWindowStack stack(m_mockCallback); + + m_onTopLevelChangedAction = [this, &stack]() { QVERIFY(stack.topWindow() == &m_root); }; + stack.pushWindow(&m_root, QWasmWindowStack::PositionPreference::Regular); + verifyWindowOrderChanged(); + + m_onTopLevelChangedAction = [this, &stack]() { QVERIFY(stack.topWindow() == &m_window1); }; + stack.pushWindow(&m_window1, QWasmWindowStack::PositionPreference::Regular); + verifyWindowOrderChanged(); + + m_onTopLevelChangedAction = [this, &stack]() { QVERIFY(stack.topWindow() == &m_window2); }; + stack.pushWindow(&m_window2, QWasmWindowStack::PositionPreference::Regular); + verifyWindowOrderChanged(); +} + +void tst_QWasmWindowStack::raising() +{ + QWasmWindowStack stack(m_mockCallback); + + stack.pushWindow(&m_root, QWasmWindowStack::PositionPreference::StayOnBottom); + stack.pushWindow(&m_window1, QWasmWindowStack::PositionPreference::Regular); + stack.pushWindow(&m_window2, QWasmWindowStack::PositionPreference::Regular); + stack.pushWindow(&m_window3, QWasmWindowStack::PositionPreference::Regular); + stack.pushWindow(&m_window4, QWasmWindowStack::PositionPreference::Regular); + stack.pushWindow(&m_window5, QWasmWindowStack::PositionPreference::Regular); + + clearCallbackCounter(); + + QCOMPARE(&m_window5, stack.topWindow()); + + m_onTopLevelChangedAction = [this, &stack]() { QVERIFY(stack.topWindow() == &m_window1); }; + stack.raise(&m_window1); + verifyWindowOrderChanged(); + QCOMPARE(&m_window1, stack.topWindow()); + + stack.raise(&m_window1); + verifyWindowOrderChanged(0); + QCOMPARE(&m_window1, stack.topWindow()); + + m_onTopLevelChangedAction = [this, &stack]() { QVERIFY(stack.topWindow() == &m_window3); }; + stack.raise(&m_window3); + verifyWindowOrderChanged(); + QCOMPARE(&m_window3, stack.topWindow()); +} + +void tst_QWasmWindowStack::raisingWithAlwaysOnBottom() +{ + QWasmWindowStack stack(m_mockCallback); + + QWasmWindow alwaysOnBottomWindow1; + QWasmWindow alwaysOnBottomWindow2; + QWasmWindow alwaysOnBottomWindow3; + + stack.pushWindow(&alwaysOnBottomWindow1, QWasmWindowStack::PositionPreference::StayOnBottom); + stack.pushWindow(&alwaysOnBottomWindow2, QWasmWindowStack::PositionPreference::StayOnBottom); + stack.pushWindow(&alwaysOnBottomWindow3, QWasmWindowStack::PositionPreference::StayOnBottom); + stack.pushWindow(&m_window1, QWasmWindowStack::PositionPreference::Regular); + stack.pushWindow(&m_window2, QWasmWindowStack::PositionPreference::Regular); + stack.pushWindow(&m_window3, QWasmWindowStack::PositionPreference::Regular); + // Window order: 3 2 1 | B3 B2 B1 + + clearCallbackCounter(); + + std::vector<QWasmWindow *> expectedWindowOrder = { &m_window3, + &m_window2, + &m_window1, + &alwaysOnBottomWindow3, + &alwaysOnBottomWindow2, + &alwaysOnBottomWindow1 }; + QVERIFY(std::equal(expectedWindowOrder.begin(), expectedWindowOrder.end(), + getWindowsFrontToBack(&stack).begin())); + QCOMPARE(&m_window3, stack.topWindow()); + + // Window order: 1 3 2 | B3 B2 B1 + stack.raise(&m_window1); + + expectedWindowOrder = { &m_window1, + &m_window3, + &m_window2, + &alwaysOnBottomWindow3, + &alwaysOnBottomWindow2, + &alwaysOnBottomWindow1 }; + QVERIFY(std::equal(expectedWindowOrder.begin(), expectedWindowOrder.end(), + getWindowsFrontToBack(&stack).begin())); + verifyWindowOrderChanged(); + QCOMPARE(&m_window1, stack.topWindow()); + + // Window order: 1 3 2 | B1 B3 B2 + stack.raise(&alwaysOnBottomWindow1); + + expectedWindowOrder = { &m_window1, + &m_window3, + &m_window2, + &alwaysOnBottomWindow1, + &alwaysOnBottomWindow3, + &alwaysOnBottomWindow2 }; + QVERIFY(std::equal(expectedWindowOrder.begin(), expectedWindowOrder.end(), + getWindowsFrontToBack(&stack).begin())); + verifyWindowOrderChanged(); + QCOMPARE(&m_window1, stack.topWindow()); + + // Window order: 1 3 2 | B3 B1 B2 + stack.raise(&alwaysOnBottomWindow3); + + expectedWindowOrder = { &m_window1, + &m_window3, + &m_window2, + &alwaysOnBottomWindow3, + &alwaysOnBottomWindow1, + &alwaysOnBottomWindow2 }; + QVERIFY(std::equal(expectedWindowOrder.begin(), expectedWindowOrder.end(), + getWindowsFrontToBack(&stack).begin())); + verifyWindowOrderChanged(); + QCOMPARE(&m_window1, stack.topWindow()); +} + +void tst_QWasmWindowStack::raisingWithAlwaysOnTop() +{ + QWasmWindowStack stack(m_mockCallback); + + QWasmWindow alwaysOnTopWindow1; + QWasmWindow alwaysOnTopWindow2; + QWasmWindow alwaysOnTopWindow3; + + stack.pushWindow(&m_root, QWasmWindowStack::PositionPreference::StayOnBottom); + stack.pushWindow(&m_window1, QWasmWindowStack::PositionPreference::Regular); + stack.pushWindow(&alwaysOnTopWindow1, QWasmWindowStack::PositionPreference::StayOnTop); + stack.pushWindow(&m_window3, QWasmWindowStack::PositionPreference::Regular); + stack.pushWindow(&alwaysOnTopWindow2, QWasmWindowStack::PositionPreference::StayOnTop); + stack.pushWindow(&m_window5, QWasmWindowStack::PositionPreference::Regular); + stack.pushWindow(&alwaysOnTopWindow3, QWasmWindowStack::PositionPreference::StayOnTop); + // Window order: T3 T2 T1 | 5 3 1 | R + + clearCallbackCounter(); + + std::vector<QWasmWindow *> expectedWindowOrder = { &alwaysOnTopWindow3, + &alwaysOnTopWindow2, + &alwaysOnTopWindow1, + &m_window5, + &m_window3, + &m_window1, + &m_root }; + QVERIFY(std::equal(expectedWindowOrder.begin(), expectedWindowOrder.end(), + getWindowsFrontToBack(&stack).begin())); + + // Window order: T3 T2 T1 | 1 5 3 | R + stack.raise(&m_window1); + + expectedWindowOrder = { &alwaysOnTopWindow3, + &alwaysOnTopWindow2, + &alwaysOnTopWindow1, + &m_window1, + &m_window5, + &m_window3, + &m_root }; + QVERIFY(std::equal(expectedWindowOrder.begin(), expectedWindowOrder.end(), + getWindowsFrontToBack(&stack).begin())); + verifyWindowOrderChanged(); + QCOMPARE(&alwaysOnTopWindow3, stack.topWindow()); + + // Window order: T3 T2 T1 3 1 5 R + stack.raise(&m_window3); + + expectedWindowOrder = { &alwaysOnTopWindow3, + &alwaysOnTopWindow2, + &alwaysOnTopWindow1, + &m_window3, + &m_window1, + &m_window5, + &m_root }; + QVERIFY(std::equal(expectedWindowOrder.begin(), expectedWindowOrder.end(), + getWindowsFrontToBack(&stack).begin())); + verifyWindowOrderChanged(); + QCOMPARE(&alwaysOnTopWindow3, stack.topWindow()); + + // Window order: T1 T3 T2 3 1 5 R + stack.raise(&alwaysOnTopWindow1); + + expectedWindowOrder = { &alwaysOnTopWindow1, + &alwaysOnTopWindow3, + &alwaysOnTopWindow2, + &m_window3, + &m_window1, + &m_window5, + &m_root }; + QVERIFY(std::equal(expectedWindowOrder.begin(), expectedWindowOrder.end(), + getWindowsFrontToBack(&stack).begin())); + verifyWindowOrderChanged(); + QCOMPARE(&alwaysOnTopWindow1, stack.topWindow()); +} + +void tst_QWasmWindowStack::lowering() +{ + QWasmWindowStack stack(m_mockCallback); + + stack.pushWindow(&m_root, QWasmWindowStack::PositionPreference::StayOnBottom); + stack.pushWindow(&m_window1, QWasmWindowStack::PositionPreference::Regular); + stack.pushWindow(&m_window2, QWasmWindowStack::PositionPreference::Regular); + stack.pushWindow(&m_window3, QWasmWindowStack::PositionPreference::Regular); + stack.pushWindow(&m_window4, QWasmWindowStack::PositionPreference::Regular); + stack.pushWindow(&m_window5, QWasmWindowStack::PositionPreference::Regular); + // Window order: 5 4 3 2 1 | R + + clearCallbackCounter(); + + QCOMPARE(&m_window5, stack.topWindow()); + + m_onTopLevelChangedAction = [this, &stack]() { QVERIFY(stack.topWindow() == &m_window4); }; + stack.lower(&m_window5); + + // Window order: 4 3 2 1 5 R + verifyWindowOrderChanged(); + QCOMPARE(&m_window4, stack.topWindow()); + + stack.lower(&m_window3); + // Window order: 4 2 1 5 3 R + verifyWindowOrderChanged(); + std::vector<QWasmWindow *> expectedWindowOrder = { &m_window4, &m_window2, &m_window1, + &m_window5, &m_window3, &m_root }; + + QVERIFY(std::equal(expectedWindowOrder.begin(), expectedWindowOrder.end(), + getWindowsFrontToBack(&stack).begin())); +} + +void tst_QWasmWindowStack::loweringWithAlwaysOnBottom() +{ + QWasmWindowStack stack(m_mockCallback); + + QWasmWindow alwaysOnBottomWindow1; + QWasmWindow alwaysOnBottomWindow2; + QWasmWindow alwaysOnBottomWindow3; + + stack.pushWindow(&alwaysOnBottomWindow1, QWasmWindowStack::PositionPreference::StayOnBottom); + stack.pushWindow(&alwaysOnBottomWindow2, QWasmWindowStack::PositionPreference::StayOnBottom); + stack.pushWindow(&alwaysOnBottomWindow3, QWasmWindowStack::PositionPreference::StayOnBottom); + stack.pushWindow(&m_window1, QWasmWindowStack::PositionPreference::Regular); + stack.pushWindow(&m_window2, QWasmWindowStack::PositionPreference::Regular); + stack.pushWindow(&m_window3, QWasmWindowStack::PositionPreference::Regular); + // Window order: 3 2 1 | B3 B2 B1 + + clearCallbackCounter(); + + std::vector<QWasmWindow *> expectedWindowOrder = { &m_window3, + &m_window2, + &m_window1, + &alwaysOnBottomWindow3, + &alwaysOnBottomWindow2, + &alwaysOnBottomWindow1 }; + QVERIFY(std::equal(expectedWindowOrder.begin(), expectedWindowOrder.end(), + getWindowsFrontToBack(&stack).begin())); + QCOMPARE(&m_window3, stack.topWindow()); + + // Window order: 2 1 3 | B3 B2 B1 + stack.lower(&m_window3); + + expectedWindowOrder = { &m_window2, + &m_window1, + &m_window3, + &alwaysOnBottomWindow3, + &alwaysOnBottomWindow2, + &alwaysOnBottomWindow1 }; + QVERIFY(std::equal(expectedWindowOrder.begin(), expectedWindowOrder.end(), + getWindowsFrontToBack(&stack).begin())); + verifyWindowOrderChanged(); + QCOMPARE(&m_window2, stack.topWindow()); + + // Window order: 2 1 3 | B2 B1 B3 + stack.lower(&alwaysOnBottomWindow3); + + expectedWindowOrder = { &m_window2, + &m_window1, + &m_window3, + &alwaysOnBottomWindow2, + &alwaysOnBottomWindow1, + &alwaysOnBottomWindow3 }; + QVERIFY(std::equal(expectedWindowOrder.begin(), expectedWindowOrder.end(), + getWindowsFrontToBack(&stack).begin())); + verifyWindowOrderChanged(); + QCOMPARE(&m_window2, stack.topWindow()); + + // Window order: 2 1 3 | B2 B3 B1 + stack.lower(&alwaysOnBottomWindow1); + + expectedWindowOrder = { &m_window2, + &m_window1, + &m_window3, + &alwaysOnBottomWindow2, + &alwaysOnBottomWindow3, + &alwaysOnBottomWindow1 }; + QVERIFY(std::equal(expectedWindowOrder.begin(), expectedWindowOrder.end(), + getWindowsFrontToBack(&stack).begin())); + verifyWindowOrderChanged(); + QCOMPARE(&m_window2, stack.topWindow()); +} + +void tst_QWasmWindowStack::loweringWithAlwaysOnTop() +{ + QWasmWindowStack stack(m_mockCallback); + + QWasmWindow alwaysOnTopWindow1; + QWasmWindow alwaysOnTopWindow2; + + stack.pushWindow(&m_root, QWasmWindowStack::PositionPreference::StayOnBottom); + stack.pushWindow(&m_window1, QWasmWindowStack::PositionPreference::Regular); + stack.pushWindow(&alwaysOnTopWindow1, QWasmWindowStack::PositionPreference::StayOnTop); + stack.pushWindow(&m_window3, QWasmWindowStack::PositionPreference::Regular); + stack.pushWindow(&alwaysOnTopWindow2, QWasmWindowStack::PositionPreference::StayOnTop); + stack.pushWindow(&m_window5, QWasmWindowStack::PositionPreference::Regular); + // Window order: T2 T1 5 3 1 R + + clearCallbackCounter(); + + std::vector<QWasmWindow *> expectedWindowOrder = { &alwaysOnTopWindow2, &alwaysOnTopWindow1, + &m_window5, &m_window3, + &m_window1, &m_root }; + QVERIFY(std::equal(expectedWindowOrder.begin(), expectedWindowOrder.end(), + getWindowsFrontToBack(&stack).begin())); + + // Window order: T1 T2 5 3 1 R + stack.lower(&alwaysOnTopWindow2); + + expectedWindowOrder = { &alwaysOnTopWindow1, &alwaysOnTopWindow2, &m_window5, + &m_window3, &m_window1, &m_root }; + QVERIFY(std::equal(expectedWindowOrder.begin(), expectedWindowOrder.end(), + getWindowsFrontToBack(&stack).begin())); + verifyWindowOrderChanged(); + QCOMPARE(&alwaysOnTopWindow1, stack.topWindow()); + + // Window order: T2 T1 5 3 1 R + stack.lower(&alwaysOnTopWindow1); + + expectedWindowOrder = { &alwaysOnTopWindow2, &alwaysOnTopWindow1, &m_window5, + &m_window3, &m_window1, &m_root }; + QVERIFY(std::equal(expectedWindowOrder.begin(), expectedWindowOrder.end(), + getWindowsFrontToBack(&stack).begin())); + verifyWindowOrderChanged(); + QCOMPARE(&alwaysOnTopWindow2, stack.topWindow()); + + // Window order: T2 T1 3 1 5 R + stack.lower(&m_window5); + + expectedWindowOrder = { &alwaysOnTopWindow2, &alwaysOnTopWindow1, &m_window3, + &m_window1, &m_window5, &m_root }; + QVERIFY(std::equal(expectedWindowOrder.begin(), expectedWindowOrder.end(), + getWindowsFrontToBack(&stack).begin())); + verifyWindowOrderChanged(); + QCOMPARE(&alwaysOnTopWindow2, stack.topWindow()); + + // Window order: T2 T1 3 5 1 R + stack.lower(&m_window1); + + expectedWindowOrder = { &alwaysOnTopWindow2, &alwaysOnTopWindow1, &m_window3, + &m_window5, &m_window1, &m_root }; + QVERIFY(std::equal(expectedWindowOrder.begin(), expectedWindowOrder.end(), + getWindowsFrontToBack(&stack).begin())); + verifyWindowOrderChanged(); + QCOMPARE(&alwaysOnTopWindow2, stack.topWindow()); +} + +void tst_QWasmWindowStack::removing() +{ + QWasmWindowStack stack(m_mockCallback); + + stack.pushWindow(&m_root, QWasmWindowStack::PositionPreference::StayOnBottom); + stack.pushWindow(&m_window1, QWasmWindowStack::PositionPreference::Regular); + stack.pushWindow(&m_window2, QWasmWindowStack::PositionPreference::Regular); + stack.pushWindow(&m_window3, QWasmWindowStack::PositionPreference::Regular); + stack.pushWindow(&m_window4, QWasmWindowStack::PositionPreference::Regular); + stack.pushWindow(&m_window5, QWasmWindowStack::PositionPreference::Regular); + // Window order: 5 4 3 2 1 R + + clearCallbackCounter(); + + QCOMPARE(&m_window5, stack.topWindow()); + + m_onTopLevelChangedAction = [this, &stack]() { QVERIFY(stack.topWindow() == &m_window4); }; + stack.removeWindow(&m_window5); + // Window order: 4 3 2 1 R + verifyWindowOrderChanged(); + QCOMPARE(&m_window4, stack.topWindow()); + + stack.removeWindow(&m_window2); + // Window order: 4 3 1 R + verifyWindowOrderChanged(); + std::vector<QWasmWindow *> expectedWindowOrder = { &m_window4, &m_window3, &m_window1, + &m_root }; + QVERIFY(std::equal(expectedWindowOrder.begin(), expectedWindowOrder.end(), + getWindowsFrontToBack(&stack).begin())); +} + +void tst_QWasmWindowStack::positionPreferenceChanges() +{ + QWasmWindowStack stack(m_mockCallback); + + QWasmWindow window6; + QWasmWindow window7; + QWasmWindow window8; + QWasmWindow window9; + + stack.pushWindow(&m_window1, QWasmWindowStack::PositionPreference::StayOnBottom); + stack.pushWindow(&m_window2, QWasmWindowStack::PositionPreference::StayOnBottom); + stack.pushWindow(&m_window3, QWasmWindowStack::PositionPreference::StayOnBottom); + stack.pushWindow(&m_window4, QWasmWindowStack::PositionPreference::Regular); + stack.pushWindow(&m_window5, QWasmWindowStack::PositionPreference::Regular); + stack.pushWindow(&window6, QWasmWindowStack::PositionPreference::Regular); + stack.pushWindow(&window7, QWasmWindowStack::PositionPreference::StayOnTop); + stack.pushWindow(&window8, QWasmWindowStack::PositionPreference::StayOnTop); + stack.pushWindow(&window9, QWasmWindowStack::PositionPreference::StayOnTop); + + // Window order: 9 8 7 | 6 5 4 | 3 2 1 + + clearCallbackCounter(); + + std::vector<QWasmWindow *> expectedWindowOrder = { &window9, &window8, &window7, + &window6, &m_window5, &m_window4, + &m_window3, &m_window2, &m_window1 }; + QVERIFY(std::equal(expectedWindowOrder.begin(), expectedWindowOrder.end(), + getWindowsFrontToBack(&stack).begin())); + + // Window order: 9 8 7 1 | 6 5 4 | 3 2 + stack.windowPositionPreferenceChanged(&m_window1, + QWasmWindowStack::PositionPreference::StayOnTop); + + expectedWindowOrder = { + &window9, &window8, &window7, &m_window1, &window6, + &m_window5, &m_window4, &m_window3, &m_window2, + }; + QVERIFY(std::equal(expectedWindowOrder.begin(), expectedWindowOrder.end(), + getWindowsFrontToBack(&stack).begin())); + + // Window order: 9 8 7 1 5 | 6 4 | 3 2 + stack.windowPositionPreferenceChanged(&m_window5, + QWasmWindowStack::PositionPreference::StayOnTop); + + expectedWindowOrder = { + &window9, &window8, &window7, &m_window1, &m_window5, + &window6, &m_window4, &m_window3, &m_window2, + }; + QVERIFY(std::equal(expectedWindowOrder.begin(), expectedWindowOrder.end(), + getWindowsFrontToBack(&stack).begin())); + + // Window order: 9 7 1 5 | 8 6 4 | 3 2 + stack.windowPositionPreferenceChanged(&window8, QWasmWindowStack::PositionPreference::Regular); + + expectedWindowOrder = { + &window9, &window7, &m_window1, &m_window5, &window8, + &window6, &m_window4, &m_window3, &m_window2, + }; + QVERIFY(std::equal(expectedWindowOrder.begin(), expectedWindowOrder.end(), + getWindowsFrontToBack(&stack).begin())); + + // Window order: 9 7 1 5 | 8 6 4 2 | 3 + stack.windowPositionPreferenceChanged(&m_window2, + QWasmWindowStack::PositionPreference::Regular); + + expectedWindowOrder = { + &window9, &window7, &m_window1, &m_window5, &window8, + &window6, &m_window4, &m_window2, &m_window3, + }; + QVERIFY(std::equal(expectedWindowOrder.begin(), expectedWindowOrder.end(), + getWindowsFrontToBack(&stack).begin())); + + // Window order: 7 1 5 | 8 6 4 2 | 9 3 + stack.windowPositionPreferenceChanged(&window9, + QWasmWindowStack::PositionPreference::StayOnBottom); + + expectedWindowOrder = { + &window7, &m_window1, &m_window5, &window8, &window6, + &m_window4, &m_window2, &window9, &m_window3, + }; + QVERIFY(std::equal(expectedWindowOrder.begin(), expectedWindowOrder.end(), + getWindowsFrontToBack(&stack).begin())); + + // Window order: 7 1 5 | 6 4 2 | 8 9 3 + stack.windowPositionPreferenceChanged(&window8, + QWasmWindowStack::PositionPreference::StayOnBottom); + + expectedWindowOrder = { + &window7, &m_window1, &m_window5, &window6, &m_window4, + &m_window2, &window8, &window9, &m_window3, + }; + QVERIFY(std::equal(expectedWindowOrder.begin(), expectedWindowOrder.end(), + getWindowsFrontToBack(&stack).begin())); +} + +void tst_QWasmWindowStack::removingWithAlwaysOnBottom() +{ + QWasmWindowStack stack(m_mockCallback); + + QWasmWindow alwaysOnBottomWindow1; + QWasmWindow alwaysOnBottomWindow2; + QWasmWindow alwaysOnBottomWindow3; + + stack.pushWindow(&alwaysOnBottomWindow1, QWasmWindowStack::PositionPreference::StayOnBottom); + stack.pushWindow(&alwaysOnBottomWindow2, QWasmWindowStack::PositionPreference::StayOnBottom); + stack.pushWindow(&alwaysOnBottomWindow3, QWasmWindowStack::PositionPreference::StayOnBottom); + stack.pushWindow(&m_window1, QWasmWindowStack::PositionPreference::Regular); + stack.pushWindow(&m_window2, QWasmWindowStack::PositionPreference::Regular); + stack.pushWindow(&m_window3, QWasmWindowStack::PositionPreference::Regular); + // Window order: 3 2 1 | B3 B2 B1 + + clearCallbackCounter(); + + std::vector<QWasmWindow *> expectedWindowOrder = { &m_window3, + &m_window2, + &m_window1, + &alwaysOnBottomWindow3, + &alwaysOnBottomWindow2, + &alwaysOnBottomWindow1 }; + QVERIFY(std::equal(expectedWindowOrder.begin(), expectedWindowOrder.end(), + getWindowsFrontToBack(&stack).begin())); + QCOMPARE(&m_window3, stack.topWindow()); + + // Window order: 3 1 | B3 B2 B1 + stack.removeWindow(&m_window2); + + expectedWindowOrder = { &m_window3, &m_window1, &alwaysOnBottomWindow3, &alwaysOnBottomWindow2, + &alwaysOnBottomWindow1 }; + QVERIFY(std::equal(expectedWindowOrder.begin(), expectedWindowOrder.end(), + getWindowsFrontToBack(&stack).begin())); + verifyWindowOrderChanged(); + QCOMPARE(&m_window3, stack.topWindow()); + + // Window order: 1 3 2 | B1 B3 B2 + stack.removeWindow(&alwaysOnBottomWindow2); + + expectedWindowOrder = { &m_window3, &m_window1, &alwaysOnBottomWindow3, + &alwaysOnBottomWindow1 }; + QVERIFY(std::equal(expectedWindowOrder.begin(), expectedWindowOrder.end(), + getWindowsFrontToBack(&stack).begin())); + verifyWindowOrderChanged(); + QCOMPARE(&m_window3, stack.topWindow()); + + // Window order: 1 3 2 | B3 B1 B2 + stack.removeWindow(&alwaysOnBottomWindow1); + + expectedWindowOrder = { &m_window3, &m_window1, &alwaysOnBottomWindow3 }; + QVERIFY(std::equal(expectedWindowOrder.begin(), expectedWindowOrder.end(), + getWindowsFrontToBack(&stack).begin())); + verifyWindowOrderChanged(); + QCOMPARE(&m_window3, stack.topWindow()); +} + +void tst_QWasmWindowStack::removingWithAlwaysOnTop() +{ + QWasmWindowStack stack(m_mockCallback); + + QWasmWindow alwaysOnTopWindow1; + QWasmWindow alwaysOnTopWindow2; + + stack.pushWindow(&m_root, QWasmWindowStack::PositionPreference::StayOnBottom); + stack.pushWindow(&m_window1, QWasmWindowStack::PositionPreference::Regular); + stack.pushWindow(&alwaysOnTopWindow1, QWasmWindowStack::PositionPreference::StayOnTop); + stack.pushWindow(&m_window3, QWasmWindowStack::PositionPreference::Regular); + stack.pushWindow(&alwaysOnTopWindow2, QWasmWindowStack::PositionPreference::StayOnTop); + stack.pushWindow(&m_window5, QWasmWindowStack::PositionPreference::Regular); + // Window order: T2 T1 5 3 1 R + + clearCallbackCounter(); + + std::vector<QWasmWindow *> expectedWindowOrder = { &alwaysOnTopWindow2, &alwaysOnTopWindow1, + &m_window5, &m_window3, + &m_window1, &m_root }; + QVERIFY(std::equal(expectedWindowOrder.begin(), expectedWindowOrder.end(), + getWindowsFrontToBack(&stack).begin())); + + // Window order: T2 T1 5 1 R + stack.removeWindow(&m_window3); + verifyWindowOrderChanged(); + + expectedWindowOrder = { &alwaysOnTopWindow2, &alwaysOnTopWindow1, &m_window5, &m_window1, + &m_root }; + QVERIFY(std::equal(expectedWindowOrder.begin(), expectedWindowOrder.end(), + getWindowsFrontToBack(&stack).begin())); + + // Window order: T2 5 1 R + stack.removeWindow(&alwaysOnTopWindow1); + verifyWindowOrderChanged(); + + expectedWindowOrder = { &alwaysOnTopWindow2, &m_window5, &m_window1, &m_root }; + QVERIFY(std::equal(expectedWindowOrder.begin(), expectedWindowOrder.end(), + getWindowsFrontToBack(&stack).begin())); + + // Window order: T2 1 R + stack.removeWindow(&m_window5); + verifyWindowOrderChanged(); + + expectedWindowOrder = { &alwaysOnTopWindow2, &m_window1, &m_root }; + QVERIFY(std::equal(expectedWindowOrder.begin(), expectedWindowOrder.end(), + getWindowsFrontToBack(&stack).begin())); + + // Window order: T2 R + stack.removeWindow(&m_window1); + verifyWindowOrderChanged(); + + expectedWindowOrder = { &alwaysOnTopWindow2, &m_root }; + QVERIFY(std::equal(expectedWindowOrder.begin(), expectedWindowOrder.end(), + getWindowsFrontToBack(&stack).begin())); + + // Window order: R + stack.removeWindow(&alwaysOnTopWindow2); + verifyWindowOrderChanged(); + QCOMPARE(&m_root, stack.topWindow()); +} + +void tst_QWasmWindowStack::clearing() +{ + QWasmWindowStack stack(m_mockCallback); + + stack.pushWindow(&m_root, QWasmWindowStack::PositionPreference::StayOnBottom); + stack.pushWindow(&m_window1, QWasmWindowStack::PositionPreference::Regular); + // Window order: 1 R + + clearCallbackCounter(); + + QCOMPARE(&m_window1, stack.topWindow()); + + m_onTopLevelChangedAction = [this, &stack]() { QVERIFY(stack.topWindow() == &m_root); }; + stack.removeWindow(&m_window1); + // Window order: R + verifyWindowOrderChanged(); + QCOMPARE(&m_root, stack.topWindow()); + + m_onTopLevelChangedAction = [&stack]() { QVERIFY(stack.topWindow() == nullptr); }; + stack.removeWindow(&m_root); + // Window order: <empty> + verifyWindowOrderChanged(); + QCOMPARE(nullptr, stack.topWindow()); + QCOMPARE(0u, stack.size()); +} + +QTEST_MAIN(tst_QWasmWindowStack) +#include "tst_qwasmwindowstack.moc" diff --git a/tests/auto/wasm/qwasmwindowtreenode/CMakeLists.txt b/tests/auto/wasm/qwasmwindowtreenode/CMakeLists.txt new file mode 100644 index 0000000000..dc292f6337 --- /dev/null +++ b/tests/auto/wasm/qwasmwindowtreenode/CMakeLists.txt @@ -0,0 +1,25 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +##################################################################### +## tst_qwasmwindowtreenode Test: +##################################################################### + +if(NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT) + cmake_minimum_required(VERSION 3.16) + project(tst_qwasmwindowtreenode LANGUAGES CXX) + find_package(Qt6BuildInternals REQUIRED COMPONENTS STANDALONE_TEST) +endif() + +qt_internal_add_test(tst_qwasmwindowtreenode + SOURCES + tst_qwasmwindowtreenode.cpp + DEFINES + QT_NO_FOREACH + LIBRARIES + Qt::GuiPrivate + PUBLIC_LIBRARIES + Qt::Core + Qt::Gui + Qt::Widgets +) diff --git a/tests/auto/wasm/qwasmwindowtreenode/tst_qwasmwindowtreenode.cpp b/tests/auto/wasm/qwasmwindowtreenode/tst_qwasmwindowtreenode.cpp new file mode 100644 index 0000000000..763dbf9a07 --- /dev/null +++ b/tests/auto/wasm/qwasmwindowtreenode/tst_qwasmwindowtreenode.cpp @@ -0,0 +1,257 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "../../../../src/plugins/platforms/wasm/qwasmwindowtreenode.h" +#include <QtGui/QWindow> +#include <QTest> +#include <emscripten/val.h> + +class QWasmWindow +{ +}; + +using OnSubtreeChangedCallback = std::function<void( + QWasmWindowTreeNodeChangeType changeType, QWasmWindowTreeNode *parent, QWasmWindow *child)>; +using SetWindowZOrderCallback = std::function<void(QWasmWindow *window, int z)>; + +struct OnSubtreeChangedCallData +{ + QWasmWindowTreeNodeChangeType changeType; + QWasmWindowTreeNode *parent; + QWasmWindow *child; +}; + +struct SetWindowZOrderCallData +{ + QWasmWindow *window; + int z; +}; + +class TestWindowTreeNode final : public QWasmWindowTreeNode, public QWasmWindow +{ +public: + TestWindowTreeNode(OnSubtreeChangedCallback onSubtreeChangedCallback, + SetWindowZOrderCallback setWindowZOrderCallback) + : m_onSubtreeChangedCallback(std::move(onSubtreeChangedCallback)), + m_setWindowZOrderCallback(std::move(setWindowZOrderCallback)) + { + } + ~TestWindowTreeNode() final { } + + void setParent(TestWindowTreeNode *parent) + { + auto *previous = m_parent; + m_parent = parent; + onParentChanged(previous, parent, QWasmWindowStack::PositionPreference::Regular); + } + + void setContainerElement(emscripten::val container) { m_containerElement = container; } + + void bringToTop() { QWasmWindowTreeNode::bringToTop(); } + + void sendToBottom() { QWasmWindowTreeNode::sendToBottom(); } + + const QWasmWindowStack &childStack() { return QWasmWindowTreeNode::childStack(); } + + emscripten::val containerElement() final { return m_containerElement; } + + QWasmWindowTreeNode *parentNode() final { return m_parent; } + + QWasmWindow *asWasmWindow() final { return this; } + +protected: + void onSubtreeChanged(QWasmWindowTreeNodeChangeType changeType, QWasmWindowTreeNode *parent, + QWasmWindow *child) final + { + m_onSubtreeChangedCallback(changeType, parent, child); + } + + void setWindowZOrder(QWasmWindow *window, int z) final { m_setWindowZOrderCallback(window, z); } + + TestWindowTreeNode *m_parent = nullptr; + emscripten::val m_containerElement = emscripten::val::undefined(); + + OnSubtreeChangedCallback m_onSubtreeChangedCallback; + SetWindowZOrderCallback m_setWindowZOrderCallback; +}; + +class tst_QWasmWindowTreeNode : public QObject +{ + Q_OBJECT + +public: + tst_QWasmWindowTreeNode() { } + +private slots: + void init(); + + void nestedWindowStacks(); + void settingChildWindowZOrder(); +}; + +void tst_QWasmWindowTreeNode::init() { } + +bool operator==(const OnSubtreeChangedCallData &lhs, const OnSubtreeChangedCallData &rhs) +{ + return lhs.changeType == rhs.changeType && lhs.parent == rhs.parent && lhs.child == rhs.child; +} + +bool operator==(const SetWindowZOrderCallData &lhs, const SetWindowZOrderCallData &rhs) +{ + return lhs.window == rhs.window && lhs.z == rhs.z; +} + +void tst_QWasmWindowTreeNode::nestedWindowStacks() +{ + QList<OnSubtreeChangedCallData> calls; + OnSubtreeChangedCallback mockOnSubtreeChanged = + [&calls](QWasmWindowTreeNodeChangeType changeType, QWasmWindowTreeNode *parent, + QWasmWindow *child) { + calls.push_back(OnSubtreeChangedCallData{ changeType, parent, child }); + }; + SetWindowZOrderCallback ignoreSetWindowZOrder = [](QWasmWindow *, int) {}; + TestWindowTreeNode node(mockOnSubtreeChanged, ignoreSetWindowZOrder); + node.bringToTop(); + + OnSubtreeChangedCallback ignoreSubtreeChanged = [](QWasmWindowTreeNodeChangeType, + QWasmWindowTreeNode *, QWasmWindow *) {}; + TestWindowTreeNode node2(ignoreSubtreeChanged, ignoreSetWindowZOrder); + node2.setParent(&node); + + QCOMPARE(node.childStack().size(), 1u); + QCOMPARE(node2.childStack().size(), 0u); + QCOMPARE(node.childStack().topWindow(), &node2); + QCOMPARE(calls.size(), 1u); + { + OnSubtreeChangedCallData expected{ QWasmWindowTreeNodeChangeType::NodeInsertion, &node, + &node2 }; + QCOMPARE(calls[0], expected); + calls.clear(); + } + + TestWindowTreeNode node3(ignoreSubtreeChanged, ignoreSetWindowZOrder); + node3.setParent(&node); + + QCOMPARE(node.childStack().size(), 2u); + QCOMPARE(node2.childStack().size(), 0u); + QCOMPARE(node3.childStack().size(), 0u); + QCOMPARE(node.childStack().topWindow(), &node3); + { + OnSubtreeChangedCallData expected{ QWasmWindowTreeNodeChangeType::NodeInsertion, &node, + &node3 }; + QCOMPARE(calls[0], expected); + calls.clear(); + } + + TestWindowTreeNode node4(ignoreSubtreeChanged, ignoreSetWindowZOrder); + node4.setParent(&node); + + QCOMPARE(node.childStack().size(), 3u); + QCOMPARE(node2.childStack().size(), 0u); + QCOMPARE(node3.childStack().size(), 0u); + QCOMPARE(node4.childStack().size(), 0u); + QCOMPARE(node.childStack().topWindow(), &node4); + { + OnSubtreeChangedCallData expected{ QWasmWindowTreeNodeChangeType::NodeInsertion, &node, + &node4 }; + QCOMPARE(calls[0], expected); + calls.clear(); + } + + node3.bringToTop(); + QCOMPARE(node.childStack().topWindow(), &node3); + + node4.setParent(nullptr); + QCOMPARE(node.childStack().size(), 2u); + QCOMPARE(node.childStack().topWindow(), &node3); + { + OnSubtreeChangedCallData expected{ QWasmWindowTreeNodeChangeType::NodeRemoval, &node, + &node4 }; + QCOMPARE(calls[0], expected); + calls.clear(); + } + + node2.setParent(nullptr); + QCOMPARE(node.childStack().size(), 1u); + QCOMPARE(node.childStack().topWindow(), &node3); + { + OnSubtreeChangedCallData expected{ QWasmWindowTreeNodeChangeType::NodeRemoval, &node, + &node2 }; + QCOMPARE(calls[0], expected); + calls.clear(); + } + + node3.setParent(nullptr); + QVERIFY(node.childStack().empty()); + QCOMPARE(node.childStack().topWindow(), nullptr); + { + OnSubtreeChangedCallData expected{ QWasmWindowTreeNodeChangeType::NodeRemoval, &node, + &node3 }; + QCOMPARE(calls[0], expected); + calls.clear(); + } +} + +void tst_QWasmWindowTreeNode::settingChildWindowZOrder() +{ + QList<SetWindowZOrderCallData> calls; + OnSubtreeChangedCallback ignoreSubtreeChanged = [](QWasmWindowTreeNodeChangeType, + QWasmWindowTreeNode *, QWasmWindow *) {}; + SetWindowZOrderCallback onSetWindowZOrder = [&calls](QWasmWindow *window, int z) { + calls.push_back(SetWindowZOrderCallData{ window, z }); + }; + SetWindowZOrderCallback ignoreSetWindowZOrder = [](QWasmWindow *, int) {}; + TestWindowTreeNode node(ignoreSubtreeChanged, onSetWindowZOrder); + + TestWindowTreeNode node2(ignoreSubtreeChanged, ignoreSetWindowZOrder); + node2.setParent(&node); + + { + QCOMPARE(calls.size(), 1u); + SetWindowZOrderCallData expected{ &node2, 3 }; + QCOMPARE(calls[0], expected); + calls.clear(); + } + + TestWindowTreeNode node3(ignoreSubtreeChanged, ignoreSetWindowZOrder); + node3.setParent(&node); + + { + QCOMPARE(calls.size(), 2u); + SetWindowZOrderCallData expected{ &node2, 3 }; + QCOMPARE(calls[0], expected); + expected = SetWindowZOrderCallData{ &node3, 4 }; + QCOMPARE(calls[1], expected); + calls.clear(); + } + + TestWindowTreeNode node4(ignoreSubtreeChanged, ignoreSetWindowZOrder); + node4.setParent(&node); + + { + QCOMPARE(calls.size(), 3u); + SetWindowZOrderCallData expected{ &node2, 3 }; + QCOMPARE(calls[0], expected); + expected = SetWindowZOrderCallData{ &node3, 4 }; + QCOMPARE(calls[1], expected); + expected = SetWindowZOrderCallData{ &node4, 5 }; + QCOMPARE(calls[2], expected); + calls.clear(); + } + + node2.bringToTop(); + + { + QCOMPARE(calls.size(), 3u); + SetWindowZOrderCallData expected{ &node3, 3 }; + QCOMPARE(calls[0], expected); + expected = SetWindowZOrderCallData{ &node4, 4 }; + QCOMPARE(calls[1], expected); + expected = SetWindowZOrderCallData{ &node2, 5 }; + QCOMPARE(calls[2], expected); + calls.clear(); + } +} + +QTEST_MAIN(tst_QWasmWindowTreeNode) +#include "tst_qwasmwindowtreenode.moc" diff --git a/tests/auto/wasm/selenium/CMakeLists.txt b/tests/auto/wasm/selenium/CMakeLists.txt new file mode 100644 index 0000000000..335b6d23d9 --- /dev/null +++ b/tests/auto/wasm/selenium/CMakeLists.txt @@ -0,0 +1,81 @@ +if(NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT) + cmake_minimum_required(VERSION 3.16) + project(tst_qwasmwindow_harness LANGUAGES CXX) + find_package(Qt6BuildInternals REQUIRED COMPONENTS STANDALONE_TEST) +endif() + +qt_internal_add_test(tst_qwasmwindow_harness + MANUAL + NO_BATCH + SOURCES + tst_qwasmwindow_harness.cpp + LIBRARIES + Qt::Core + Qt::Gui + Qt::OpenGL + Qt::Widgets +) + +# Resources: +set(shaders_resource_files + "fshader.glsl" + "vshader.glsl" +) + +qt6_add_resources(tst_qwasmwindow_harness "shaders" + PREFIX + "/" + FILES + ${shaders_resource_files} +) + +if(CMAKE_HOST_WIN32) + SET(RUNSHCMD run.bat) + SET(RUNSHARG "NotUsed") +else() + SET(RUNSHCMD bash) + SET(RUNSHARG run.sh) +endif() + +set_target_properties(tst_qwasmwindow_harness PROPERTIES CROSSCOMPILING_EMULATOR "") # disabling emrun +qt_internal_create_test_script(NAME tst_qwasmwindow_harness + COMMAND ${RUNSHCMD} + ARGS ${RUNSHARG} + WORKING_DIRECTORY "${test_working_dir}" + OUTPUT_FILE "${CMAKE_CURRENT_BINARY_DIR}/tst_qwasmwindow_harnessWrapper$<CONFIG>.cmake" +) + +add_custom_command( + TARGET tst_qwasmwindow_harness POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + ${CMAKE_CURRENT_SOURCE_DIR}/tst_qwasmwindow_harness.html + ${CMAKE_CURRENT_BINARY_DIR}/tst_qwasmwindow_harness.html +) + +add_custom_command( + TARGET tst_qwasmwindow_harness POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../util/wasm/qtwasmserver/qtwasmserver.py + ${CMAKE_CURRENT_BINARY_DIR}/qtwasmserver.py +) + +add_custom_command( + TARGET tst_qwasmwindow_harness POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + ${CMAKE_CURRENT_SOURCE_DIR}/qwasmwindow.py + ${CMAKE_CURRENT_BINARY_DIR}/qwasmwindow.py +) + +add_custom_command( + TARGET tst_qwasmwindow_harness POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + ${CMAKE_CURRENT_SOURCE_DIR}/run.sh + ${CMAKE_CURRENT_BINARY_DIR}/run.sh +) + +add_custom_command( + TARGET tst_qwasmwindow_harness POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + ${CMAKE_CURRENT_SOURCE_DIR}/run.bat + ${CMAKE_CURRENT_BINARY_DIR}/run.bat +) diff --git a/tests/auto/wasm/selenium/fshader.glsl b/tests/auto/wasm/selenium/fshader.glsl new file mode 100644 index 0000000000..252735f91c --- /dev/null +++ b/tests/auto/wasm/selenium/fshader.glsl @@ -0,0 +1,21 @@ +// Copyright (C) 2018 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifdef GL_ES +// Set default precision to medium +precision mediump int; +precision mediump float; +#endif + +uniform sampler2D texture; + +varying vec2 v_texcoord; + +//! [0] +void main() +{ + // Set fragment color from texture + gl_FragColor = texture2D(texture, v_texcoord); +} +//! [0] + diff --git a/tests/auto/wasm/selenium/qwasmwindow.py b/tests/auto/wasm/selenium/qwasmwindow.py new file mode 100644 index 0000000000..260e9d2d24 --- /dev/null +++ b/tests/auto/wasm/selenium/qwasmwindow.py @@ -0,0 +1,1042 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +from selenium.webdriver import Chrome +from selenium.webdriver.chrome.service import Service as ChromeService +from selenium.webdriver.common.action_chains import ActionChains +from selenium.webdriver.common.actions.action_builder import ActionBuilder +from selenium.webdriver.common.actions.pointer_actions import PointerActions +from selenium.webdriver.common.actions.interaction import POINTER_TOUCH +from selenium.webdriver.common.actions.pointer_input import PointerInput +from selenium.webdriver.common.by import By +from selenium.webdriver.support.expected_conditions import presence_of_element_located +from selenium.webdriver.support.ui import WebDriverWait +from webdriver_manager.chrome import ChromeDriverManager + +import time +import unittest +from enum import Enum, auto + +class WidgetTestCase(unittest.TestCase): + def setUp(self): + self._driver = Chrome(service=ChromeService(ChromeDriverManager().install())) + self._driver.get( + 'http://localhost:8001/tst_qwasmwindow_harness.html') + self._test_sandbox_element = WebDriverWait(self._driver, 30).until( + presence_of_element_located((By.ID, 'test-sandbox')) + ) + self.addTypeEqualityFunc(Color, assert_colors_equal) + self.addTypeEqualityFunc(Rect, assert_rects_equal) + + def test_hasFocus_returnsFalse_whenSetNoFocusShowWasCalled(self): + screen = Screen(self._driver, ScreenPosition.FIXED, + x=0, y=0, width=600, height=1200) + + w0 = Widget(self._driver, "w0") + w0.show() + self.assertEqual(w0.hasFocus(), True) + + w1 = Widget(self._driver, "w1") + w1.setNoFocusShow() + w1.show() + self.assertEqual(w0.hasFocus(), True) + self.assertEqual(w1.hasFocus(), False) + + w2 = Widget(self._driver, "w2") + w2.show() + self.assertEqual(w0.hasFocus(), False) + self.assertEqual(w1.hasFocus(), False) + self.assertEqual(w2.hasFocus(), True) + + w3 = Widget(self._driver, "w3") + w3.setNoFocusShow() + w3.show() + self.assertEqual(w0.hasFocus(), False) + self.assertEqual(w1.hasFocus(), False) + self.assertEqual(w2.hasFocus(), True) + self.assertEqual(w3.hasFocus(), False) + w3.activate(); + self.assertEqual(w0.hasFocus(), False) + self.assertEqual(w1.hasFocus(), False) + self.assertEqual(w2.hasFocus(), False) + self.assertEqual(w3.hasFocus(), True) + + clearWidgets(self._driver) + + def test_window_resizing(self): + screen = Screen(self._driver, ScreenPosition.FIXED, + x=0, y=0, width=600, height=600) + + window = Window(parent=screen, rect=Rect(x=100, y=100, width=200, height=200)) + self.assertEqual(window.rect, Rect(x=100, y=100, width=200, height=200)) + + window.drag(Handle.TOP_LEFT, direction=UP(10) + LEFT(10)) + self.assertEqual(window.rect, Rect(x=90, y=90, width=210, height=210)) + + window.drag(Handle.TOP, direction=DOWN(10) + LEFT(100)) + self.assertEqual(window.rect, Rect(x=90, y=100, width=210, height=200)) + + window.drag(Handle.TOP_RIGHT, direction=UP(5) + LEFT(5)) + self.assertEqual(window.rect, Rect(x=90, y=95, width=205, height=205)) + + window.drag(Handle.RIGHT, direction=DOWN(100) + RIGHT(5)) + self.assertEqual(window.rect, Rect(x=90, y=95, width=210, height=205)) + + window.drag(Handle.BOTTOM_RIGHT, direction=UP(5) + LEFT(10)) + self.assertEqual(window.rect, Rect(x=90, y=95, width=200, height=200)) + + window.drag(Handle.BOTTOM, direction=DOWN(20) + LEFT(100)) + self.assertEqual(window.rect, Rect(x=90, y=95, width=200, height=220)) + + window.drag(Handle.BOTTOM_LEFT, direction=DOWN(10) + LEFT(10)) + self.assertEqual(window.rect, Rect(x=80, y=95, width=210, height=230)) + + window.drag(Handle.LEFT, direction=DOWN(343) + LEFT(5)) + self.assertEqual(window.rect, Rect(x=75, y=95, width=215, height=230)) + + window.drag(Handle.BOTTOM_RIGHT, direction=UP(150) + LEFT(150)) + self.assertEqual(window.rect, Rect(x=75, y=95, width=65, height=80)) + + def test_cannot_resize_over_screen_top_edge(self): + screen = Screen(self._driver, ScreenPosition.FIXED, + x=200, y=200, width=300, height=300) + window = Window(parent=screen, rect=Rect(x=300, y=300, width=100, height=100)) + self.assertEqual(window.rect, Rect(x=300, y=300, width=100, height=100)) + frame_rect_before_resize = window.frame_rect + + window.drag(Handle.TOP, direction=UP(200)) + self.assertEqual(window.rect.x, 300) + self.assertEqual(window.frame_rect.y, screen.rect.y) + self.assertEqual(window.rect.width, 100) + self.assertEqual(window.frame_rect.y + window.frame_rect.height, + frame_rect_before_resize.y + frame_rect_before_resize.height) + + def test_window_move(self): + screen = Screen(self._driver, ScreenPosition.FIXED, + x=200, y=200, width=300, height=300) + window = Window(parent=screen, rect=Rect(x=300, y=300, width=100, height=100)) + self.assertEqual(window.rect, Rect(x=300, y=300, width=100, height=100)) + + window.drag(Handle.TOP_WINDOW_BAR, direction=UP(30)) + self.assertEqual(window.rect, Rect(x=300, y=270, width=100, height=100)) + + window.drag(Handle.TOP_WINDOW_BAR, direction=RIGHT(50)) + self.assertEqual(window.rect, Rect(x=350, y=270, width=100, height=100)) + + window.drag(Handle.TOP_WINDOW_BAR, direction=DOWN(30) + LEFT(70)) + self.assertEqual(window.rect, Rect(x=280, y=300, width=100, height=100)) + + def test_screen_limits_window_moves(self): + screen = Screen(self._driver, ScreenPosition.RELATIVE, + x=200, y=200, width=300, height=300) + window = Window(parent=screen, rect=Rect(x=300, y=300, width=100, height=100)) + self.assertEqual(window.rect, Rect(x=300, y=300, width=100, height=100)) + + window.drag(Handle.TOP_WINDOW_BAR, direction=LEFT(300)) + self.assertEqual(window.frame_rect.x, screen.rect.x - window.frame_rect.width / 2) + + def test_screen_in_scroll_container_limits_window_moves(self): + screen = Screen(self._driver, ScreenPosition.IN_SCROLL_CONTAINER, + x=200, y=2000, width=300, height=300, + container_width=500, container_height=7000) + screen.scroll_to() + window = Window(parent=screen, rect=Rect(x=300, y=2100, width=100, height=100)) + self.assertEqual(window.rect, Rect(x=300, y=2100, width=100, height=100)) + + window.drag(Handle.TOP_WINDOW_BAR, direction=LEFT(300)) + self.assertEqual(window.frame_rect.x, screen.rect.x - window.frame_rect.width / 2) + + def test_maximize(self): + screen = Screen(self._driver, ScreenPosition.RELATIVE, + x=200, y=200, width=300, height=300) + window = Window(parent=screen, rect=Rect(x=300, y=300, width=100, height=100), title='Maximize') + self.assertEqual(window.rect, Rect(x=300, y=300, width=100, height=100)) + + window.maximize() + self.assertEqual(window.frame_rect, Rect(x=200, y=200, width=300, height=300)) + + def test_multitouch_window_move(self): + screen = Screen(self._driver, ScreenPosition.FIXED, + x=0, y=0, width=800, height=800) + windows = [Window(screen, rect=Rect(x=50, y=50, width=100, height=100), title='First'), + Window(screen, rect=Rect(x=400, y=400, width=100, height=100), title='Second'), + Window(screen, rect=Rect(x=50, y=400, width=100, height=100), title='Third')] + + self.assertEqual(windows[0].rect, Rect(x=50, y=50, width=100, height=100)) + self.assertEqual(windows[1].rect, Rect(x=400, y=400, width=100, height=100)) + self.assertEqual(windows[2].rect, Rect(x=50, y=400, width=100, height=100)) + + actions = [TouchDragAction(origin=windows[0].at(Handle.TOP_WINDOW_BAR), direction=DOWN(20) + RIGHT(20)), + TouchDragAction(origin=windows[1].at(Handle.TOP_WINDOW_BAR), direction=DOWN(20) + LEFT(20)), + TouchDragAction(origin=windows[2].at(Handle.TOP_WINDOW_BAR), direction=UP(20) + RIGHT(20))] + perform_touch_drag_actions(actions) + self.assertEqual(windows[0].rect, Rect(x=70, y=70, width=100, height=100)) + self.assertEqual(windows[1].rect, Rect(x=380, y=420, width=100, height=100)) + self.assertEqual(windows[2].rect, Rect(x=70, y=380, width=100, height=100)) + + #TODO FIX IN CI + @unittest.skip('Skip temporarily') + def test_multitouch_window_resize(self): + screen = Screen(self._driver, ScreenPosition.FIXED, + x=0, y=0, width=800, height=800) + windows = [Window(screen, rect=Rect(x=50, y=50, width=150, height=150), title='First'), + Window(screen, rect=Rect(x=400, y=400, width=150, height=150), title='Second'), + Window(screen, rect=Rect(x=50, y=400, width=150, height=150), title='Third')] + + self.assertEqual(windows[0].rect, Rect(x=50, y=50, width=150, height=150)) + self.assertEqual(windows[1].rect, Rect(x=400, y=400, width=150, height=150)) + self.assertEqual(windows[2].rect, Rect(x=50, y=400, width=150, height=150)) + + actions = [TouchDragAction(origin=windows[0].at(Handle.TOP_LEFT), direction=DOWN(20) + RIGHT(20)), + TouchDragAction(origin=windows[1].at(Handle.TOP), direction=DOWN(20) + LEFT(20)), + TouchDragAction(origin=windows[2].at(Handle.BOTTOM_RIGHT), direction=UP(20) + RIGHT(20))] + perform_touch_drag_actions(actions) + self.assertEqual(windows[0].rect, Rect(x=70, y=70, width=130, height=130)) + self.assertEqual(windows[1].rect, Rect(x=400, y=420, width=150, height=130)) + self.assertEqual(windows[2].rect, Rect(x=50, y=400, width=170, height=130)) + + def test_newly_created_window_gets_keyboard_focus(self): + screen = Screen(self._driver, ScreenPosition.FIXED, + x=0, y=0, width=800, height=800) + window = Window(parent=screen, rect=Rect(x=0, y=0, width=800, height=800), title='root') + + ActionChains(self._driver).key_down('c').key_up('c').perform() + + events = window.events + self.assertEqual(len(events), 2) + self.assertEqual(events[-2]['type'], 'keyPress') + self.assertEqual(events[-2]['key'], 'c') + self.assertEqual(events[-1]['type'], 'keyRelease') + self.assertEqual(events[-1]['key'], 'c') + + #TODO FIX IN CI + @unittest.skip('Does not work in CI') + def test_child_window_activation(self): + screen = Screen(self._driver, ScreenPosition.FIXED, + x=0, y=0, width=800, height=800) + + root = Window(parent=screen, rect=Rect(x=0, y=0, width=800, height=800), title='root') + w1 = Window(parent=root, rect=Rect(x=100, y=100, width=600, height=600), title='w1') + w1_w1 = Window(parent=w1, rect=Rect(x=100, y=100, width=300, height=300), title='w1_w1') + w1_w1_w1 = Window(parent=w1_w1, rect=Rect(x=100, y=100, width=100, height=100), title='w1_w1_w1') + w1_w1_w2 = Window(parent=w1_w1, rect=Rect(x=150, y=150, width=100, height=100), title='w1_w1_w2') + w1_w2 = Window(parent=w1, rect=Rect(x=300, y=300, width=300, height=300), title='w1_w2') + w1_w2_w1 = Window(parent=w1_w2, rect=Rect(x=100, y=100, width=100, height=100), title='w1_w2_w1') + w2 = Window(parent=root, rect=Rect(x=300, y=300, width=450, height=450), title='w2') + + self.assertEqual(screen.window_stack_at_point(*w1_w1.bounding_box.center), + [w2, w1_w1_w2, w1_w1_w1, w1_w1, w1, root]) + + self.assertEqual(screen.window_stack_at_point(*w2.bounding_box.center), + [w2, w1_w2_w1, w1_w2, w1, root]) + + for w in [w1, w1_w1, w1_w1_w1, w1_w1_w2, w1_w2, w1_w2_w1]: + self.assertFalse(w.active) + self.assertTrue(w2.active) + + w1.click(0, 0) + + for w in [w1, w1_w2, w1_w2_w1]: + self.assertTrue(w.active) + for w in [w1_w1, w1_w1_w1, w1_w1_w2, w2]: + self.assertFalse(w.active) + + self.assertEqual(screen.window_stack_at_point(*w2.bounding_box.center), + [w1_w2_w1, w1_w2, w1, w2, root]) + + w1_w1_w1.click(0, 0) + + for w in [w1, w1_w1, w1_w1_w1]: + self.assertTrue(w.active) + for w in [w1_w1_w2, w1_w2, w1_w2_w1, w2]: + self.assertFalse(w.active) + + self.assertEqual(screen.window_stack_at_point(*w1_w1_w1.bounding_box.center), + [w1_w1_w1, w1_w1_w2, w1_w1, w1, w2, root]) + + w1_w1_w2.click(w1_w1_w2.bounding_box.width, w1_w1_w2.bounding_box.height) + + for w in [w1, w1_w1, w1_w1_w2]: + self.assertTrue(w.active) + for w in [w1_w1_w1, w1_w2, w1_w2_w1, w2]: + self.assertFalse(w.active) + + self.assertEqual(screen.window_stack_at_point(w1_w1_w2.bounding_box.x, w1_w1_w2.bounding_box.y), + [w1_w1_w2, w1_w1_w1, w1_w1, w1, w2, root]) + + def test_window_reparenting(self): + screen = Screen(self._driver, ScreenPosition.FIXED, + x=0, y=0, width=800, height=800) + + bottom = Window(parent=screen, rect=Rect(x=800, y=800, width=300, height=300), title='bottom') + w1 = Window(parent=screen, rect=Rect(x=50, y=50, width=300, height=300), title='w1') + w2 = Window(parent=screen, rect=Rect(x=50, y=50, width=300, height=300), title='w2') + w3 = Window(parent=screen, rect=Rect(x=50, y=50, width=300, height=300), title='w3') + + self.assertTrue( + w2.element not in [*w1.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w3.element not in [*w1.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w1.element not in [*w2.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w3.element not in [*w2.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w1.element not in [*w3.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w2.element not in [*w3.element.find_elements(By.XPATH, "ancestor::div")]) + + w2.set_parent(w1) + + self.assertTrue( + w2.element not in [*w1.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w3.element not in [*w1.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w1.element in [*w2.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w3.element not in [*w2.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w1.element not in [*w3.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w2.element not in [*w3.element.find_elements(By.XPATH, "ancestor::div")]) + + w3.set_parent(w2) + + self.assertTrue( + w2.element not in [*w1.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w3.element not in [*w1.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w1.element in [*w2.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w3.element not in [*w2.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w1.element in [*w3.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w2.element in [*w3.element.find_elements(By.XPATH, "ancestor::div")]) + + w2.set_parent(screen) + + self.assertTrue( + w2.element not in [*w1.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w3.element not in [*w1.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w1.element not in [*w2.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w3.element not in [*w2.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w1.element not in [*w3.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w2.element in [*w3.element.find_elements(By.XPATH, "ancestor::div")]) + + w1.set_parent(w2) + + self.assertTrue( + w2.element in [*w1.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w3.element not in [*w1.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w1.element not in [*w2.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w3.element not in [*w2.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w1.element not in [*w3.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w2.element in [*w3.element.find_elements(By.XPATH, "ancestor::div")]) + + w3.set_parent(screen) + + self.assertTrue( + w2.element in [*w1.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w3.element not in [*w1.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w1.element not in [*w2.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w3.element not in [*w2.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w1.element not in [*w3.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w2.element not in [*w3.element.find_elements(By.XPATH, "ancestor::div")]) + + w2.set_parent(w3) + + self.assertTrue( + w2.element in [*w1.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w3.element in [*w1.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w1.element not in [*w2.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w3.element in [*w2.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w1.element not in [*w3.element.find_elements(By.XPATH, "ancestor::div")]) + self.assertTrue( + w2.element not in [*w3.element.find_elements(By.XPATH, "ancestor::div")]) + + def test_window_closing(self): + screen = Screen(self._driver, ScreenPosition.FIXED, + x=0, y=0, width=800, height=800) + + bottom = Window(parent=screen, rect=Rect(x=800, y=800, width=300, height=300), title='root') + bottom.close() + + w1 = Window(parent=screen, rect=Rect(x=50, y=50, width=300, height=300), title='w1') + w2 = Window(parent=screen, rect=Rect(x=50, y=50, width=300, height=300), title='w2') + w3 = Window(parent=screen, rect=Rect(x=50, y=50, width=300, height=300), title='w3') + + w3.close() + + self.assertFalse(w3 in screen.query_windows()) + self.assertTrue(w2 in screen.query_windows()) + self.assertTrue(w1 in screen.query_windows()) + + w4 = Window(parent=screen, rect=Rect(x=50, y=50, width=300, height=300), title='w4') + + self.assertTrue(w4 in screen.query_windows()) + self.assertTrue(w2 in screen.query_windows()) + self.assertTrue(w1 in screen.query_windows()) + + w2.close() + w1.close() + + self.assertTrue(w4 in screen.query_windows()) + self.assertFalse(w2 in screen.query_windows()) + self.assertFalse(w1 in screen.query_windows()) + + w4.close() + + self.assertFalse(w4 in screen.query_windows()) + + def test_window_painting(self): + screen = Screen(self._driver, ScreenPosition.FIXED, + x=0, y=0, width=800, height=800) + bottom = Window(parent=screen, rect=Rect(x=0, y=0, width=400, height=400), title='root') + bottom.set_background_color(Color(r=255, g=0, b=0)) + wait_for_animation_frame(self._driver) + + self.assertEqual(bottom.color_at(0, 0), Color(r=255, g=0, b=0)) + + w1 = Window(parent=screen, rect=Rect(x=100, y=100, width=600, height=600), title='w1') + w1.set_background_color(Color(r=0, g=255, b=0)) + wait_for_animation_frame(self._driver) + + self.assertEqual(w1.color_at(0, 0), Color(r=0, g=255, b=0)) + + w1_w1 = Window(parent=screen, rect=Rect(x=100, y=100, width=400, height=400), title='w1_w1') + w1_w1.set_parent(w1) + w1_w1.set_background_color(Color(r=0, g=0, b=255)) + wait_for_animation_frame(self._driver) + + self.assertEqual(w1_w1.color_at(0, 0), Color(r=0, g=0, b=255)) + + w1_w1_w1 = Window(parent=screen, rect=Rect(x=100, y=100, width=200, height=200), title='w1_w1_w1') + w1_w1_w1.set_parent(w1_w1) + w1_w1_w1.set_background_color(Color(r=255, g=255, b=0)) + wait_for_animation_frame(self._driver) + + self.assertEqual(w1_w1_w1.color_at(0, 0), Color(r=255, g=255, b=0)) + + def test_opengl_painting(self): + screen = Screen(self._driver, ScreenPosition.FIXED, + x=0, y=0, width=800, height=800) + bottom = Window(parent=screen, rect=Rect(x=0, y=0, width=400, height=400), title='root',opengl=1) + bottom.set_background_color(Color(r=255, g=0, b=0)) + wait_for_animation_frame(self._driver) + time.sleep(1) + + self.assertEqual(bottom.window_color_at_0_0(), Color(r=255, g=0, b=0)) + + w1 = Window(parent=screen, rect=Rect(x=100, y=100, width=600, height=600), title='w1', opengl=1) + w1.set_background_color(Color(r=0, g=255, b=0)) + wait_for_animation_frame(self._driver) + time.sleep(1) + + self.assertEqual(w1.window_color_at_0_0(), Color(r=0, g=255, b=0)) + + w1_w1 = Window(parent=screen, rect=Rect(x=100, y=100, width=400, height=400), title='w1_w1', opengl=1) + w1_w1.set_parent(w1) + w1_w1.set_background_color(Color(r=0, g=0, b=255)) + wait_for_animation_frame(self._driver) + time.sleep(1) + + self.assertEqual(w1_w1.window_color_at_0_0(), Color(r=0, g=0, b=255)) + + w1_w1_w1 = Window(parent=screen, rect=Rect(x=100, y=100, width=200, height=200), title='w1_w1_w1', opengl=1) + w1_w1_w1.set_parent(w1_w1) + w1_w1_w1.set_background_color(Color(r=255, g=255, b=0)) + wait_for_animation_frame(self._driver) + time.sleep(1) + + self.assertEqual(w1_w1_w1.window_color_at_0_0(), Color(r=255, g=255, b=0)) + +#TODO FIX IN CI + @unittest.skip('Does not work in CI') + def test_keyboard_input(self): + screen = Screen(self._driver, ScreenPosition.FIXED, + x=0, y=0, width=800, height=800) + + bottom = Window(parent=screen, rect=Rect(x=0, y=0, width=800, height=800), title='root') + w1 = Window(parent=screen, rect=Rect(x=100, y=100, width=600, height=600), title='w1') + w1_w1 = Window(parent=w1, rect=Rect(x=100, y=100, width=400, height=400), title='w1_w1') + w1_w1_w1 = Window(parent=w1_w1, rect=Rect(x=100, y=100, width=100, height=100), title='w1_w1_w1') + Window(parent=w1_w1, rect=Rect(x=150, y=150, width=100, height=100), title='w1_w1_w2') + + w1_w1_w1.click(0, 0) + + ActionChains(self._driver).key_down('c').key_up('c').perform() + + events = w1_w1_w1.events + self.assertEqual(len(events), 2) + self.assertEqual(events[-2]['type'], 'keyPress') + self.assertEqual(events[-2]['key'], 'c') + self.assertEqual(events[-1]['type'], 'keyRelease') + self.assertEqual(events[-1]['key'], 'c') + self.assertEqual(len(w1_w1.events), 0) + self.assertEqual(len(w1.events), 0) + + w1_w1.click(0, 0) + + ActionChains(self._driver).key_down('b').key_up('b').perform() + + events = w1_w1.events + self.assertEqual(len(events), 2) + self.assertEqual(events[-2]['type'], 'keyPress') + self.assertEqual(events[-2]['key'], 'b') + self.assertEqual(events[-1]['type'], 'keyRelease') + self.assertEqual(events[-1]['key'], 'b') + self.assertEqual(len(w1_w1_w1.events), 2) + self.assertEqual(len(w1.events), 0) + + w1.click(0, 0) + + ActionChains(self._driver).key_down('a').key_up('a').perform() + + events = w1.events + self.assertEqual(len(events), 2) + self.assertEqual(events[-2]['type'], 'keyPress') + self.assertEqual(events[-2]['key'], 'a') + self.assertEqual(events[-1]['type'], 'keyRelease') + self.assertEqual(events[-1]['key'], 'a') + self.assertEqual(len(w1_w1_w1.events), 2) + self.assertEqual(len(w1_w1.events), 2) + + def tearDown(self): + self._driver.quit() + +class ScreenPosition(Enum): + FIXED = auto() + RELATIVE = auto() + IN_SCROLL_CONTAINER = auto() + +class Screen: + def __init__(self, driver, positioning=None, x=None, y=None, width=None, height=None, container_width=0, container_height=0, screen_name=None): + self.driver = driver + if screen_name is not None: + screen_information = call_instance_function(self.driver, 'screenInformation') + if len(screen_information) != 1: + raise AssertionError('Expecting exactly one screen_information!') + self.screen_info = screen_information[0] + self.element = driver.find_element(By.CSS_SELECTOR, f'#test-screen-1') + return + + if positioning == ScreenPosition.FIXED: + command = f'initializeScreenWithFixedPosition({x}, {y}, {width}, {height})' + elif positioning == ScreenPosition.RELATIVE: + command = f'initializeScreenWithRelativePosition({x}, {y}, {width}, {height})' + elif positioning == ScreenPosition.IN_SCROLL_CONTAINER: + command = f'initializeScreenInScrollContainer({container_width}, {container_height}, {x}, {y}, {width}, {height})' + self.element = self.driver.execute_script( + f''' + return testSupport.{command}; + ''' + ) + if positioning == ScreenPosition.IN_SCROLL_CONTAINER: + self.element = self.element[1] + + screen_information = call_instance_function( + self.driver, 'screenInformation') + if len(screen_information) != 1: + raise AssertionError('Expecting exactly one screen_information!') + self.screen_info = screen_information[0] + + @property + def rect(self): + self.screen_info = call_instance_function( + self.driver, 'screenInformation')[0] + geo = self.screen_info['geometry'] + return Rect(geo['x'], geo['y'], geo['width'], geo['height']) + + @property + def name(self): + return self.screen_info['name'] + + def scroll_to(self): + ActionChains(self.driver).scroll_to_element(self.element).perform() + + def hit_test_point(self, x, y): + return self.driver.execute_script( + f''' + return testSupport.hitTestPoint({x}, {y}, '{self.element.get_attribute("id")}'); + ''' + ) + + def window_stack_at_point(self, x, y): + return [ + Window(self, element=element) for element in [ + *filter(lambda elem: (elem.get_attribute('id') if elem.get_attribute('id') is not None else '') + .startswith('qt-window-'), self.hit_test_point(x, y))]] + + def query_windows(self): + shadow_container = self.element.find_element(By.CSS_SELECTOR, f'#qt-shadow-container') + return [ + Window(self, element=element) for element in shadow_container.shadow_root.find_elements( + By.CSS_SELECTOR, f'div#{self.name} > div.qt-window')] + + def find_element(self, method, query): + shadow_container = self.element.find_element(By.CSS_SELECTOR, f'#qt-shadow-container') + return shadow_container.shadow_root.find_element(method, query) + +def clearWidgets(driver): + driver.execute_script( + f''' + instance.clearWidgets(); + ''' + ) + +class Widget: + def __init__(self, driver, name): + self.name=name + self.driver=driver + + self.driver.execute_script( + f''' + instance.createWidget('{self.name}'); + ''' + ) + + def setNoFocusShow(self): + self.driver.execute_script( + f''' + instance.setWidgetNoFocusShow('{self.name}'); + ''' + ) + + def show(self): + self.driver.execute_script( + f''' + instance.showWidget('{self.name}'); + ''' + ) + def hasFocus(self): + focus = call_instance_function_arg(self.driver, 'hasWidgetFocus', self.name) + return focus + + def activate(self): + self.driver.execute_script( + f''' + instance.activateWidget('{self.name}'); + ''' + ) + + +class Window: + def __init__(self, parent=None, rect=None, title=None, element=None, visible=True, opengl=0): + self.driver = parent.driver + self.opengl = opengl + if element is not None: + self.element = element + self.title = element.find_element( + By.CSS_SELECTOR, f'.title-bar > .window-name').get_property("textContent") + information = self.__window_information() + self.screen = Screen(self.driver, screen_name=information['screen']['name']) + pass + else: + self.title = title = title if title is not None else 'window' + if isinstance(parent, Window): + self.driver.execute_script( + f''' + instance.createWindow({rect.x}, {rect.y}, {rect.width}, {rect.height}, 'window', '{parent.title}', '{title}', {opengl}); + ''' + ) + self.screen = parent.screen + else: + assert(isinstance(parent, Screen)) + self.driver.execute_script( + f''' + instance.createWindow({rect.x}, {rect.y}, {rect.width}, {rect.height}, 'screen', '{parent.name}', '{title}', {opengl}); + ''' + ) + self.screen = parent + self._window_id = self.__window_information()['id'] + self.element = self.screen.find_element( + By.CSS_SELECTOR, f'#qt-window-{self._window_id}') + if visible: + self.set_visible(True) + + def __eq__(self, other): + return self._window_id == other._window_id if isinstance(other, Window) else False + + def __window_information(self): + information = call_instance_function(self.driver, 'windowInformation') + return next(filter(lambda e: e['title'] == self.title, information)) + + @property + def rect(self): + geo = self.__window_information()["geometry"] + return Rect(geo['x'], geo['y'], geo['width'], geo['height']) + + @property + def frame_rect(self): + geo = self.__window_information()["frameGeometry"] + return Rect(geo['x'], geo['y'], geo['width'], geo['height']) + + @property + def events(self): + events = self.driver.execute_script( + f''' + return testSupport.events(); + ''' + ) + return [*filter(lambda e: e['windowTitle'] == self.title, events)] + + def set_visible(self, visible): + info = self.__window_information() + self.driver.execute_script( + f'''instance.setWindowVisible({info['id']}, {'true' if visible else 'false'});''') + + def drag(self, handle, direction): + ActionChains(self.driver) \ + .move_to_element_with_offset(self.element, *self.at(handle)['offset']) \ + .click_and_hold() \ + .move_by_offset(*translate_direction_to_offset(direction)) \ + .release().perform() + + def maximize(self): + maximize_button = self.element.find_element( + By.CSS_SELECTOR, f'.title-bar :nth-child(6)') + maximize_button.click() + + def at(self, handle): + """ Returns (window, offset) for given handle on window""" + width = self.frame_rect.width + height = self.frame_rect.height + + if handle == Handle.TOP_LEFT: + offset = (-width/2, -height/2) + elif handle == Handle.TOP: + offset = (0, -height/2) + elif handle == Handle.TOP_RIGHT: + offset = (width/2, -height/2) + elif handle == Handle.LEFT: + offset = (-width/2, 0) + elif handle == Handle.RIGHT: + offset = (width/2, 0) + elif handle == Handle.BOTTOM_LEFT: + offset = (-width/2, height/2) + elif handle == Handle.BOTTOM: + offset = (0, height/2) + elif handle == Handle.BOTTOM_RIGHT: + offset = (width/2, height/2) + elif handle == Handle.TOP_WINDOW_BAR: + frame_top = self.frame_rect.y + client_area_top = self.rect.y + top_frame_bar_width = client_area_top - frame_top + offset = (0, -height/2 + top_frame_bar_width/2) + return {'window': self, 'offset': offset} + + @property + def bounding_box(self): + raw = self.driver.execute_script(""" + return arguments[0].getBoundingClientRect(); + """, self.element) + return Rect(raw['x'], raw['y'], raw['width'], raw['height']) + + @property + def active(self): + return not self.inactive + # self.assertFalse('inactive' in window_element.get_attribute( + # 'class').split(' '), window_element.get_attribute('id')) + + @property + def inactive(self): + window_chain = [ + *self.element.find_elements(By.XPATH, "ancestor::div"), self.element] + return next(filter(lambda elem: 'qt-window' in elem.get_attribute('class').split(' ') and + 'inactive' in elem.get_attribute( + 'class').split(' '), + window_chain + ), None) is not None + + def click(self, x, y): + rect = self.bounding_box + + SELENIUM_IMPRECISION_COMPENSATION = 2 + ActionChains(self.driver).move_to_element( + self.element).move_by_offset(-rect.width / 2 + x + SELENIUM_IMPRECISION_COMPENSATION, + -rect.height / 2 + y + SELENIUM_IMPRECISION_COMPENSATION).click().perform() + + def set_parent(self, parent): + if isinstance(parent, Screen): + # TODO won't work with screen that is not parent.screen + self.screen = parent + self.driver.execute_script( + f''' + instance.setWindowParent('{self.title}', 'none'); + ''' + ) + else: + assert(isinstance(parent, Window)) + self.screen = parent.screen + self.driver.execute_script( + f''' + instance.setWindowParent('{self.title}', '{parent.title}'); + ''' + ) + + def close(self): + self.driver.execute_script( + f''' + instance.closeWindow('{self.title}'); + ''' + ) + + def window_color_at_0_0(self): + color = call_instance_function_arg(self.driver, 'getOpenGLColorAt_0_0', self.title) + + wcol = color[0] + r = wcol['r'] + g = wcol['g'] + b = wcol['b'] + + return Color(r,g,b) + + def color_at(self, x, y): + raw = self.driver.execute_script( + f''' + return arguments[0].querySelector('canvas') + .getContext('2d').getImageData({x}, {y}, 1, 1).data; + ''', self.element) + return Color(r=raw[0], g=raw[1], b=raw[2]) + + def set_background_color(self, color): + return self.driver.execute_script( + f''' + return instance.setWindowBackgroundColor('{self.title}', {color.r}, {color.g}, {color.b}); + ''' + ) + + +class TouchDragAction: + def __init__(self, origin, direction): + self.origin = origin + self.direction = direction + self.step = 2 + + +def perform_touch_drag_actions(actions): + driver = actions[0].origin['window'].driver + touch_action_builder = ActionBuilder(driver) + pointers = [PointerActions(source=touch_action_builder.add_pointer_input( + POINTER_TOUCH, f'touch_input_{i}')) for i in range(len(actions))] + + for action, pointer in zip(actions, pointers): + pointer.move_to( + action.origin['window'].element, *action.origin['offset']) + pointer.pointer_down(width=10, height=10, pressure=1) + moves = [translate_direction_to_offset(a.direction) for a in actions] + + def movement_finished(): + for move in moves: + if move != (0, 0): + return False + return True + + def sign(num): + if num > 0: + return 1 + elif num < 0: + return -1 + return 0 + + while not movement_finished(): + for i in range(len(actions)): + pointer = pointers[i] + move = moves[i] + step = actions[i].step + + current_move = ( + min(abs(move[0]), step) * sign(move[0]), min(abs(move[1]), step) * sign(move[1])) + moves[i] = (move[0] - current_move[0], move[1] - current_move[1]) + pointer.move_by(current_move[0], + current_move[1], width=10, height=10) + for pointer in pointers: + pointer.pointer_up() + + touch_action_builder.perform() + + +class TouchDragAction: + def __init__(self, origin, direction): + self.origin = origin + self.direction = direction + self.step = 2 + + +def perform_touch_drag_actions(actions): + driver = actions[0].origin['window'].driver + touch_action_builder = ActionBuilder(driver) + pointers = [PointerActions(source=touch_action_builder.add_pointer_input( + POINTER_TOUCH, f'touch_input_{i}')) for i in range(len(actions))] + + for action, pointer in zip(actions, pointers): + pointer.move_to( + action.origin['window'].element, *action.origin['offset']) + pointer.pointer_down(width=10, height=10, pressure=1) + + moves = [translate_direction_to_offset(a.direction) for a in actions] + + def movement_finished(): + for move in moves: + if move != (0, 0): + return False + return True + + def sign(num): + if num > 0: + return 1 + elif num < 0: + return -1 + return 0 + + while not movement_finished(): + for i in range(len(actions)): + pointer = pointers[i] + move = moves[i] + step = actions[i].step + + current_move = ( + min(abs(move[0]), step) * sign(move[0]), min(abs(move[1]), step) * sign(move[1])) + moves[i] = (move[0] - current_move[0], move[1] - current_move[1]) + pointer.move_by(current_move[0], + current_move[1], width=10, height=10) + + for pointer in pointers: + pointer.pointer_up() + + touch_action_builder.perform() + + +def translate_direction_to_offset(direction): + return (direction.val[1] - direction.val[3], direction.val[2] - direction.val[0]) + + +def call_instance_function(driver, name): + return driver.execute_script( + f'''let result; + window.{name}Callback = data => result = data; + instance.{name}(); + return eval(result);''') + +def call_instance_function_arg(driver, name, arg): + return driver.execute_script( + f'''let result; + window.{name}Callback = data => result = data; + instance.{name}('{arg}'); + return eval(result);''') + +def wait_for_animation_frame(driver): + driver.execute_script( + ''' + window.requestAnimationFrame(() => { + const sync = document.createElement('div'); + sync.id = 'test-sync'; + document.body.appendChild(sync); + }); + ''' + ) + WebDriverWait(driver, 1).until( + presence_of_element_located((By.ID, 'test-sync')) + ) + driver.execute_script( + ''' + document.body.removeChild(document.body.querySelector('#test-sync')); + ''' + ) + +class Direction: + def __init__(self): + self.val = (0, 0, 0, 0) + + def __init__(self, north, east, south, west): + self.val = (north, east, south, west) + + def __add__(self, other): + return Direction(self.val[0] + other.val[0], + self.val[1] + other.val[1], + self.val[2] + other.val[2], + self.val[3] + other.val[3]) + + +class UP(Direction): + def __init__(self, step=1): + self.val = (step, 0, 0, 0) + + +class RIGHT(Direction): + def __init__(self, step=1): + self.val = (0, step, 0, 0) + + +class DOWN(Direction): + def __init__(self, step=1): + self.val = (0, 0, step, 0) + + +class LEFT(Direction): + def __init__(self, step=1): + self.val = (0, 0, 0, step) + + +class Handle(Enum): + TOP_LEFT = auto() + TOP = auto() + TOP_RIGHT = auto() + LEFT = auto() + RIGHT = auto() + BOTTOM_LEFT = auto() + BOTTOM = auto() + BOTTOM_RIGHT = auto() + TOP_WINDOW_BAR = auto() + +class Color: + def __init__(self, r, g, b): + self.r = r + self.g = g + self.b = b + +class Rect: + def __init__(self, x, y, width, height) -> None: + self.x = x + self.y = y + self.width = width + self.height = height + + def __str__(self): + return f'(x: {self.x}, y: {self.y}, width: {self.width}, height: {self.height})' + + @property + def center(self): + return self.x + self.width / 2, self.y + self.height / 2, + +def assert_colors_equal(color1, color2, msg=None): + if color1.r != color2.r or color1.g != color2.g or color1.b != color2.b: + raise AssertionError(f'Colors not equal: \n{color1} \nvs \n{color2}') + +def assert_rects_equal(geo1, geo2, msg=None): + if geo1.x != geo2.x or geo1.y != geo2.y or geo1.width != geo2.width or geo1.height != geo2.height: + raise AssertionError(f'Rectangles not equal: \n{geo1} \nvs \n{geo2}') + +unittest.main() diff --git a/tests/auto/wasm/selenium/run.bat b/tests/auto/wasm/selenium/run.bat new file mode 100644 index 0000000000..031e0b13ab --- /dev/null +++ b/tests/auto/wasm/selenium/run.bat @@ -0,0 +1,7 @@ +:: +:: The highest version of python that can be used is 3.11 +:: Download from here: https://www.python.org/downloads/release/python-3118/ +:: +start "qtwasmserver.py" python qtwasmserver.py -p 8001 +python qwasmwindow.py +taskkill /FI "WINDOWTITLE eq qtwasmserver.py" diff --git a/tests/auto/wasm/selenium/run.sh b/tests/auto/wasm/selenium/run.sh new file mode 100755 index 0000000000..b9a41ace8c --- /dev/null +++ b/tests/auto/wasm/selenium/run.sh @@ -0,0 +1,58 @@ +#! /bin/bash + +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +set -m + +function removeServer() +{ + [ -z "$cleanupPid" ] || kill $cleanupPid +} +trap removeServer EXIT + +function compare_python_versions() { + python_version=$1 + required_version=$2 + if [ "$(printf "%s\n" "$required_version" "$python_version" | sort -V | head -n 1)" != "$required_version" ]; then + return 0 # python version is too old + else + return 1 # python version is legit + fi +} + +python_command="python3" +# At least python 3.7 is required for Selenium 4 +if command -v python3 &> /dev/null; then + python_version=$(python3 --version 2>&1 | awk '{print $2}') + + if compare_python_versions "$python_version" "3.7"; then # if Python is older then 3.7, look for newer versions + newer_python="" + for version in 3.7 3.8 3.9 3.10 3.11; do + potential_python=$(command -v "python$version") + if [ -n "$potential_python" ]; then + newer_python=$potential_python + newer_version=$($newer_python --version 2>&1 | awk '{print $2}') + break + fi + done + + if [ -n "$newer_python" ]; then # if newer version is found, use it instead + newer_version=$($newer_python --version 2>&1 | awk '{print $2}') + python_command=$newer_python + else + echo "Error: At least Python3.7 is required, currently installed version: Python$python_version" + exit 1 + fi + fi +else + echo "Error: Python3 not installed" + exit 1 +fi + +script_dir=`dirname ${BASH_SOURCE[0]}` +cd "$script_dir" +$python_command qtwasmserver.py -p 8001 > /dev/null 2>&1 & +cleanupPid=$! + +$python_command qwasmwindow.py $@ diff --git a/tests/auto/wasm/selenium/shaders.qrc b/tests/auto/wasm/selenium/shaders.qrc new file mode 100644 index 0000000000..bfc4b25111 --- /dev/null +++ b/tests/auto/wasm/selenium/shaders.qrc @@ -0,0 +1,6 @@ +<RCC> + <qresource prefix="/"> + <file>vshader.glsl</file> + <file>fshader.glsl</file> + </qresource> +</RCC> diff --git a/tests/auto/wasm/selenium/tst_qwasmwindow_harness.cpp b/tests/auto/wasm/selenium/tst_qwasmwindow_harness.cpp new file mode 100644 index 0000000000..365fc74a34 --- /dev/null +++ b/tests/auto/wasm/selenium/tst_qwasmwindow_harness.cpp @@ -0,0 +1,696 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include <QtCore/QEvent> +#include <QtWidgets/qwidget.h> + +#include <QtGui/qevent.h> +#include <QtCore/qobject.h> +#include <QtCore/qregularexpression.h> +#include <QtGui/qpainter.h> +#include <QtGui/qrasterwindow.h> +#include <QtGui/qscreen.h> +#include <QtGui/qwindow.h> +#include <QtGui/qguiapplication.h> +#include <QtWidgets/qlineedit.h> +#include <QApplication> +#include <QDialog> +#include <QSysInfo> + +#include <QOpenGLWindow> +#include <QOpenGLFunctions> +#include <QOpenGLShaderProgram> + +#include <emscripten.h> +#include <emscripten/bind.h> +#include <emscripten/val.h> + +#include <memory> +#include <sstream> +#include <vector> + +class TestWindowBase +{ +public: + virtual ~TestWindowBase() {} + virtual void setBackgroundColor(int r, int g, int b) = 0; + virtual void setVisible(bool visible) = 0; + virtual void setParent(QWindow *parent) = 0; + virtual bool close() = 0; + virtual QWindow *qWindow() = 0; + virtual void opengl_color_at_0_0(int *r, int *g, int *b) = 0; +}; + +class TestWidget : public QDialog +{ + Q_OBJECT +}; + +class TestWindow : public QRasterWindow, public TestWindowBase +{ + Q_OBJECT + +public: + virtual void setBackgroundColor(int r, int g, int b) override final + { + m_backgroundColor = QColor::fromRgb(r, g, b); + update(); + } + virtual void setVisible(bool visible) override final + { + QRasterWindow::setVisible(visible); + } + virtual void setParent(QWindow *parent) override final + { + QRasterWindow::setParent(parent); + } + virtual bool close() override final + { + return QRasterWindow::close(); + } + virtual QWindow *qWindow() override final + { + return static_cast<QRasterWindow *>(this); + } + virtual void opengl_color_at_0_0(int *r, int *g, int *b) override final + { + *r = 0; + *g = 0; + *b = 0; + } + +private: + void closeEvent(QCloseEvent *ev) override final + { + Q_UNUSED(ev); + delete this; + } + + void keyPressEvent(QKeyEvent *event) final + { + auto data = emscripten::val::object(); + data.set("type", emscripten::val("keyPress")); + data.set("windowId", emscripten::val(winId())); + data.set("windowTitle", emscripten::val(title().toStdString())); + data.set("key", emscripten::val(event->text().toStdString())); + emscripten::val::global("window")["testSupport"].call<void>("reportEvent", std::move(data)); + } + + void keyReleaseEvent(QKeyEvent *event) final + { + auto data = emscripten::val::object(); + data.set("type", emscripten::val("keyRelease")); + data.set("windowId", emscripten::val(winId())); + data.set("windowTitle", emscripten::val(title().toStdString())); + data.set("key", emscripten::val(event->text().toStdString())); + emscripten::val::global("window")["testSupport"].call<void>("reportEvent", std::move(data)); + } + + void paintEvent(QPaintEvent *e) final + { + QPainter painter(this); + painter.fillRect(e->rect(), m_backgroundColor); + } + + QColor m_backgroundColor = Qt::white; +}; + +class ContextGuard +{ +public: + ContextGuard(QOpenGLContext *context, QSurface *surface) : m_context(context) + { + m_contextMutex.lock(); + m_context->makeCurrent(surface); + } + + ~ContextGuard() + { + m_context->doneCurrent(); + m_contextMutex.unlock(); + } + +private: + QOpenGLContext *m_context = nullptr; + static std::mutex m_contextMutex; +}; + +std::mutex ContextGuard::m_contextMutex; + +class TestOpenGLWindow : public QWindow, QOpenGLFunctions, public TestWindowBase +{ + Q_OBJECT + +public: + TestOpenGLWindow() + { + setSurfaceType(OpenGLSurface); + create(); + + // + // Create the texture in the share context + // + m_shareContext = std::make_shared<QOpenGLContext>(); + m_shareContext->create(); + + { + ContextGuard guard(m_shareContext.get(), this); + initializeOpenGLFunctions(); + + m_shaderProgram = std::make_shared<QOpenGLShaderProgram>(); + + if (!m_shaderProgram->addShaderFromSourceFile(QOpenGLShader::Vertex, ":/vshader.glsl") + || !m_shaderProgram->addShaderFromSourceFile(QOpenGLShader::Fragment, + ":/fshader.glsl") + || !m_shaderProgram->link() || !m_shaderProgram->bind()) { + + qDebug() << " Build problem"; + qDebug() << "Log " << m_shaderProgram->log(); + + m_shaderProgram = nullptr; + } else { + m_shaderProgram->setUniformValue("texture", 0); + } + + // + // Texture + // + glGenTextures(1, &m_TextureId); + glBindTexture(GL_TEXTURE_2D, m_TextureId); + + uint8_t pixel[4] = { 255, 255, 255, 128 }; + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixel); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + + const GLfloat triangleData[] = { -1.0, -1.0, 0.0, 0.5, 0.5, 1.0, -1.0, 0.0, + 0.5, 0.5, -1.0, 1.0, 0.0, 0.5, 0.5 }; + const GLushort indices[] = { 0, 1, 2 }; + + glGenBuffers(1, &m_vertexBufferId); + glBindBuffer(GL_ARRAY_BUFFER, m_vertexBufferId); + glBufferData(GL_ARRAY_BUFFER, sizeof(float[5]) * 3, &triangleData, GL_STATIC_DRAW); + + glGenBuffers(1, &m_indexBufferId); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_indexBufferId); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLushort) * 3, indices, GL_STATIC_DRAW); + + glBindBuffer(GL_ARRAY_BUFFER, m_vertexBufferId); + + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(float[5]), 0); + + glEnableVertexAttribArray(1); + glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(float[5]), (void *)(12)); + } + + // + // We will use the texture in this context + // + m_context = std::make_shared<QOpenGLContext>(); + m_context->setShareContext(m_shareContext.get()); + m_context->create(); + + { + ContextGuard guard(m_context.get(), this); + initializeOpenGLFunctions(); + + glBindTexture(GL_TEXTURE_2D, m_TextureId); + glBindBuffer(GL_ARRAY_BUFFER, m_vertexBufferId); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_indexBufferId); + m_shaderProgram->bind(); + + // Tell OpenGL programmable pipeline how to locate vertex position data + const int vertexLocation = m_shaderProgram->attributeLocation("a_position"); + m_shaderProgram->enableAttributeArray(vertexLocation); + m_shaderProgram->setAttributeBuffer(vertexLocation, GL_FLOAT, 0, 3, sizeof(float[5])); + + // Tell OpenGL programmable pipeline how to locate vertex texture coordinate data + const int texcoordLocation = m_shaderProgram->attributeLocation("a_texcoord"); + m_shaderProgram->enableAttributeArray(texcoordLocation); + m_shaderProgram->setAttributeBuffer(texcoordLocation, GL_FLOAT, sizeof(float[3]), 2, + sizeof(float[5])); + } + + renderLater(); + } + +public: + virtual void setBackgroundColor(int red, int green, int blue) override final + { + { + ContextGuard guard(m_shareContext.get(), this); + + // + // Update texture + // + const uint8_t pixel[4] = { (uint8_t)red, (uint8_t)green, (uint8_t)blue, 128 }; + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixel); + } + + renderLater(); + } + virtual void setVisible(bool visible) override final { QWindow::setVisible(visible); } + virtual void setParent(QWindow *parent) override final { QWindow::setParent(parent); } + virtual bool close() override final { return QWindow::close(); } + virtual QWindow *qWindow() override final { return static_cast<QWindow *>(this); } + virtual void opengl_color_at_0_0(int *r, int *g, int *b) override final + { + ContextGuard guard(m_context.get(), this); + + *r = m_rgba[0]; + *g = m_rgba[1]; + *b = m_rgba[2]; + } + +private: + bool event(QEvent *event) override final + { + switch (event->type()) { + case QEvent::UpdateRequest: + renderNow(); + return true; + default: + return QWindow::event(event); + } + } + + void exposeEvent(QExposeEvent *event) override final + { + Q_UNUSED(event); + + if (isExposed()) + renderNow(); + } + + void closeEvent(QCloseEvent *ev) override final + { + Q_UNUSED(ev); + delete this; + } + + void keyPressEvent(QKeyEvent *event) override final + { + auto data = emscripten::val::object(); + data.set("type", emscripten::val("keyPress")); + data.set("windowId", emscripten::val(winId())); + data.set("windowTitle", emscripten::val(title().toStdString())); + data.set("key", emscripten::val(event->text().toStdString())); + emscripten::val::global("window")["testSupport"].call<void>("reportEvent", std::move(data)); + } + + void keyReleaseEvent(QKeyEvent *event) override final + { + auto data = emscripten::val::object(); + data.set("type", emscripten::val("keyRelease")); + data.set("windowId", emscripten::val(winId())); + data.set("windowTitle", emscripten::val(title().toStdString())); + data.set("key", emscripten::val(event->text().toStdString())); + emscripten::val::global("window")["testSupport"].call<void>("reportEvent", std::move(data)); + } + void renderLater() { requestUpdate(); } + void renderNow() + { + qDebug() << " Render now"; + ContextGuard guard(m_context.get(), this); + const auto sz = size(); + glViewport(0, 0, sz.width(), sz.height()); + + glClearColor(1, 1, 1, 1); + glClear(GL_COLOR_BUFFER_BIT); + + // Draw triangle using indices from VBO + glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_SHORT, nullptr); + + glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, m_rgba); + m_context->swapBuffers(this); + } + +private: + std::shared_ptr<QOpenGLShaderProgram> m_shaderProgram; + GLuint m_vertexBufferId = 0; + GLuint m_indexBufferId = 0; + GLuint m_TextureId = 0; + + std::shared_ptr<QOpenGLContext> m_shareContext; + std::shared_ptr<QOpenGLContext> m_context; + uint8_t m_rgba[4]; // Color at location(0, 0) +}; + +namespace { +TestWindowBase *findWindowByTitle(const std::string &title) +{ + auto windows = qGuiApp->allWindows(); + auto window_it = std::find_if(windows.begin(), windows.end(), [&title](QWindow *window) { + return window->title() == QString::fromLatin1(title); + }); + return window_it == windows.end() ? nullptr : dynamic_cast<TestWindowBase *>(*window_it); +} + +class WidgetStorage +{ +private: + ~WidgetStorage() + { + } +public: + static WidgetStorage *getInstance() + { + if (!s_instance) + { + s_instance = new WidgetStorage(); + } + return s_instance; + } + static void clearInstance() + { + delete s_instance; + s_instance = nullptr; + } + + TestWidget *findWidget(const std::string &name) + { + auto it = m_widgets.find(name); + if (it != m_widgets.end()) + return it->second.get(); + return nullptr; + } + + QLineEdit *findEdit(const std::string &name) + { + auto it = m_lineEdits.find(name); + if (it != m_lineEdits.end()) + return it->second; + return nullptr; + } + + void make(const std::string &name) + { + auto widget = std::make_shared<TestWidget>(); + auto *lineEdit = new QLineEdit(widget.get()); + + widget->setMinimumSize(200, 200); + widget->setMaximumSize(200, 200); + widget->setGeometry(0, m_widgetY, 200, 200); + m_widgetY += 200; + + lineEdit->setText("Hello world"); + + m_widgets[name] = widget; + m_lineEdits[name] = lineEdit; + } + +private: + using TestWidgetPtr = std::shared_ptr<TestWidget>; + + static WidgetStorage * s_instance; + std::map<std::string, TestWidgetPtr> m_widgets; + std::map<std::string, QLineEdit *> m_lineEdits; + int m_widgetY = 0; +}; + +WidgetStorage *WidgetStorage::s_instance = nullptr; + +} // namespace + +using namespace emscripten; + +std::string toJSArray(const std::vector<std::string> &elements) +{ + std::ostringstream out; + out << "["; + bool comma = false; + for (const auto &element : elements) { + out << (comma ? "," : ""); + out << element; + comma = true; + } + out << "]"; + return out.str(); +} + +std::string toJSString(const QString &qstring) +{ + Q_ASSERT_X(([qstring]() { + static QRegularExpression unescapedQuoteRegex(R"re((?:^|[^\\])')re"); + return qstring.indexOf(unescapedQuoteRegex) == -1; + })(), + Q_FUNC_INFO, "Unescaped single quotes found"); + return "'" + qstring.toStdString() + "'"; +} + +std::string rectToJSObject(const QRect &rect) +{ + std::ostringstream out; + out << "{" + << " x: " << std::to_string(rect.x()) << "," + << " y: " << std::to_string(rect.y()) << "," + << " width: " << std::to_string(rect.width()) << "," + << " height: " << std::to_string(rect.height()) << "}"; + return out.str(); +} + +std::string screenToJSObject(const QScreen &screen) +{ + std::ostringstream out; + out << "{" + << " name: " << toJSString(screen.name()) << "," + << " geometry: " << rectToJSObject(screen.geometry()) << "}"; + return out.str(); +} + +std::string windowToJSObject(const QWindow &window) +{ + std::ostringstream out; + out << "{" + << " id: " << std::to_string(window.winId()) << "," + << " geometry: " << rectToJSObject(window.geometry()) << "," + << " frameGeometry: " << rectToJSObject(window.frameGeometry()) << "," + << " screen: " << screenToJSObject(*window.screen()) << "," + << " title: '" << window.title().toStdString() << "' }"; + return out.str(); +} + +void windowInformation() +{ + auto windows = qGuiApp->allWindows(); + + std::vector<std::string> windowsAsJsObjects; + windowsAsJsObjects.reserve(windows.size()); + std::transform(windows.begin(), windows.end(), std::back_inserter(windowsAsJsObjects), + [](const QWindow *window) { return windowToJSObject(*window); }); + + emscripten::val::global("window").call<void>("windowInformationCallback", + emscripten::val(toJSArray(windowsAsJsObjects))); +} + +void screenInformation() +{ + auto screens = qGuiApp->screens(); + + std::vector<std::string> screensAsJsObjects; + screensAsJsObjects.reserve(screens.size()); + std::transform(screens.begin(), screens.end(), std::back_inserter(screensAsJsObjects), + [](const QScreen *screen) { return screenToJSObject(*screen); }); + emscripten::val::global("window").call<void>("screenInformationCallback", + emscripten::val(toJSArray(screensAsJsObjects))); +} + +void createWidget(const std::string &name) +{ + WidgetStorage::getInstance()->make(name); +} + +void setWidgetNoFocusShow(const std::string &name) +{ + auto w = WidgetStorage::getInstance()->findWidget(name); + if (w) + w->setAttribute(Qt::WA_ShowWithoutActivating); +} + +void showWidget(const std::string &name) +{ + auto w = WidgetStorage::getInstance()->findWidget(name); + if (w) + w->show(); +} + +void hasWidgetFocus(const std::string &name) +{ + bool focus = false; + auto le = WidgetStorage::getInstance()->findEdit(name); + if (le) + focus = le->hasFocus(); + + emscripten::val::global("window").call<void>("hasWidgetFocusCallback", + emscripten::val(focus)); +} + +void activateWidget(const std::string &name) +{ + auto w = WidgetStorage::getInstance()->findWidget(name); + if (w) + w->activateWindow(); +} + +void clearWidgets() +{ + WidgetStorage::clearInstance(); +} + +void createWindow(int x, int y, int w, int h, const std::string &parentType, const std::string &parentId, + const std::string &title, bool opengl) +{ + QScreen *parentScreen = nullptr; + QWindow *parentWindow = nullptr; + if (parentType == "screen") { + auto screens = qGuiApp->screens(); + auto screen_it = std::find_if(screens.begin(), screens.end(), [&parentId](QScreen *screen) { + return screen->name() == QString::fromLatin1(parentId); + }); + if (screen_it == screens.end()) { + qWarning() << "No such screen: " << parentId; + return; + } + parentScreen = *screen_it; + } else if (parentType == "window") { + auto testWindow = findWindowByTitle(parentId); + + if (!testWindow) { + qWarning() << "No parent window: " << parentId; + return; + } + parentWindow = testWindow->qWindow(); + parentScreen = parentWindow->screen(); + } else { + qWarning() << "Wrong parent type " << parentType; + return; + } + + if (opengl) { + qDebug() << "Making OpenGL window"; + auto window = new TestOpenGLWindow; + window->setFlag(Qt::WindowTitleHint); + window->setFlag(Qt::WindowMaximizeButtonHint); + window->setTitle(QString::fromLatin1(title)); + window->setGeometry(x, y, w, h); + window->setScreen(parentScreen); + window->setParent(parentWindow); + } else { + qDebug() << "Making Raster window"; + auto window = new TestWindow; + window->setFlag(Qt::WindowTitleHint); + window->setFlag(Qt::WindowMaximizeButtonHint); + window->setTitle(QString::fromLatin1(title)); + window->setGeometry(x, y, w, h); + window->setScreen(parentScreen); + window->setParent(parentWindow); + } +} + +void setWindowBackgroundColor(const std::string &title, int r, int g, int b) +{ + auto *window = findWindowByTitle(title); + if (!window) { + qWarning() << "No such window: " << title; + return; + } + window->setBackgroundColor(r, g, b); +} + +void setWindowVisible(int windowId, bool visible) +{ + auto windows = qGuiApp->allWindows(); + auto window_it = std::find_if(windows.begin(), windows.end(), [windowId](QWindow *window) { + return window->winId() == WId(windowId); + }); + if (window_it == windows.end()) { + qWarning() << "No such window: " << windowId; + return; + } + + (*window_it)->setVisible(visible); +} + +void setWindowParent(const std::string &windowTitle, const std::string &parentTitle) +{ + TestWindowBase *window = findWindowByTitle(windowTitle); + if (!window) { + qWarning() << "Window could not be found " << windowTitle; + return; + } + TestWindowBase *parent = nullptr; + if (parentTitle != "none") { + if ((parent = findWindowByTitle(parentTitle)) == nullptr) { + qWarning() << "Parent window could not be found " << parentTitle; + return; + } + } + window->setParent(parent ? parent->qWindow() : nullptr); +} + +bool closeWindow(const std::string &title) +{ + TestWindowBase *window = findWindowByTitle(title); + return window ? window->close() : false; +} + +std::string colorToJs(int r, int g, int b) +{ + return + "[{" + " r: " + std::to_string(r) + "," + " g: " + std::to_string(g) + "," + " b: " + std::to_string(b) + "" + "}]"; +} + +void getOpenGLColorAt_0_0(const std::string &windowTitle) +{ + TestWindowBase *window = findWindowByTitle(windowTitle); + int r = 0; + int g = 0; + int b = 0; + + if (!window) { + qWarning() << "Window could not be found " << windowTitle; + } else { + window->opengl_color_at_0_0(&r, &g, &b); + } + + emscripten::val::global("window").call<void>("getOpenGLColorAt_0_0Callback", + emscripten::val(colorToJs(r, g, b))); +} + +EMSCRIPTEN_BINDINGS(qwasmwindow) +{ + emscripten::function("screenInformation", &screenInformation); + emscripten::function("windowInformation", &windowInformation); + + emscripten::function("createWindow", &createWindow); + emscripten::function("setWindowVisible", &setWindowVisible); + emscripten::function("setWindowParent", &setWindowParent); + emscripten::function("closeWindow", &closeWindow); + emscripten::function("setWindowBackgroundColor", &setWindowBackgroundColor); + + emscripten::function("getOpenGLColorAt_0_0", &getOpenGLColorAt_0_0); + + emscripten::function("createWidget", &createWidget); + emscripten::function("setWidgetNoFocusShow", &setWidgetNoFocusShow); + emscripten::function("showWidget", &showWidget); + emscripten::function("activateWidget", &activateWidget); + emscripten::function("hasWidgetFocus", &hasWidgetFocus); + emscripten::function("clearWidgets", &clearWidgets); +} + +int main(int argc, char **argv) +{ + QApplication app(argc, argv); + + app.exec(); + return 0; +} + +#include "tst_qwasmwindow_harness.moc" diff --git a/tests/auto/wasm/selenium/tst_qwasmwindow_harness.html b/tests/auto/wasm/selenium/tst_qwasmwindow_harness.html new file mode 100644 index 0000000000..8c2457f302 --- /dev/null +++ b/tests/auto/wasm/selenium/tst_qwasmwindow_harness.html @@ -0,0 +1,80 @@ +<!doctype html> + +<head> + <script type="text/javascript" src="tst_qwasmwindow_harness.js"></script> + <script> + (async () => { + const instance = await tst_qwasmwindow_harness_entry({}); + window.instance = instance; + + const testSandbox = document.createElement('div'); + testSandbox.id = 'test-sandbox'; + let nextScreenId = 1; + document.body.appendChild(testSandbox); + + const eventList = []; + + const makeSizedDiv = (left, top, width, height) => { + const screenDiv = document.createElement('div'); + + screenDiv.style.left = `${left}px`; + screenDiv.style.top = `${top}px`; + screenDiv.style.width = `${width}px`; + screenDiv.style.height = `${height}px`; + screenDiv.style.backgroundColor = 'lightblue'; + screenDiv.id = `test-screen-${nextScreenId++}`; + + return screenDiv; + }; + + window.testSupport = { + initializeScreenWithFixedPosition: (left, top, width, height) => { + const screenDiv = makeSizedDiv(left, top, width, height); + testSandbox.appendChild(screenDiv); + + screenDiv.style.position = 'fixed'; + instance.qtAddContainerElement(screenDiv); + + return screenDiv; + }, + initializeScreenWithRelativePosition: (left, top, width, height) => { + const screenDiv = makeSizedDiv(left, top, width, height); + testSandbox.appendChild(screenDiv); + + screenDiv.style.position = 'relative'; + instance.qtAddContainerElement(screenDiv); + + return screenDiv; + }, + initializeScreenInScrollContainer: + (scrollWidth, scrollHeight, left, top, width, height) => { + const scrollContainer = document.createElement('div'); + scrollContainer.style.height = `${scrollHeight}px`; + scrollContainer.style.width = `${scrollWidth}px`; + testSandbox.appendChild(scrollContainer); + + const screenDiv = makeSizedDiv(left, top, width, height); + scrollContainer.appendChild(screenDiv); + screenDiv.style.position = 'relative'; + + instance.qtAddContainerElement(screenDiv); + + return [scrollContainer, screenDiv]; + }, + reportEvent: event => { + eventList.push(event); + }, + events: () => eventList, + hitTestPoint: (x, y, screenId) => { + return document + .querySelector(`#${screenId}`) + .querySelector('#qt-shadow-container') + .shadowRoot.elementsFromPoint(x, y); + } + }; + })(); + </script> +</head> + +<body> +</body> diff --git a/tests/auto/wasm/selenium/vshader.glsl b/tests/auto/wasm/selenium/vshader.glsl new file mode 100644 index 0000000000..95e2bab607 --- /dev/null +++ b/tests/auto/wasm/selenium/vshader.glsl @@ -0,0 +1,27 @@ +// Copyright (C) 2018 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifdef GL_ES +// Set default precision to medium +precision mediump int; +precision mediump float; +#endif + +uniform mat4 mvp_matrix; + +attribute vec4 a_position; +attribute vec2 a_texcoord; + +varying vec2 v_texcoord; + +//! [0] +void main() +{ + // Calculate vertex position in screen space + gl_Position = a_position; + + // Pass texture coordinate to fragment shader + // Value will be automatically interpolated to fragments inside polygon faces + v_texcoord = a_texcoord; +} +//! [0] diff --git a/tests/auto/wasm/tst_qstdweb.cpp b/tests/auto/wasm/tst_qstdweb.cpp deleted file mode 100644 index f074866a3d..0000000000 --- a/tests/auto/wasm/tst_qstdweb.cpp +++ /dev/null @@ -1,629 +0,0 @@ -// Copyright (C) 2022 The Qt Company Ltd. -// Copyright (C) 2016 Intel Corporation. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 - -#include <QtCore/qtimer.h> -#include <QtCore/private/qstdweb_p.h> -#include <QTest> -#include <emscripten.h> -#include <emscripten/bind.h> -#include <emscripten/val.h> - -#if defined(QT_HAVE_EMSCRIPTEN_ASYNCIFY) -#define SKIP_IF_NO_ASYNCIFY() -#else -#define SKIP_IF_NO_ASYNCIFY() QSKIP("Needs QT_HAVE_EMSCRIPTEN_ASYNCIFY") -#endif - -using namespace emscripten; - -class tst_QStdWeb : public QObject -{ - Q_OBJECT -public: - tst_QStdWeb() : m_window(val::global("window")), m_testSupport(val::object()) { - instance = this; - - m_window.set("testSupport", m_testSupport); - } - ~tst_QStdWeb() noexcept {} - -private: - static tst_QStdWeb* instance; - - void init() { - EM_ASM({ - testSupport.resolve = {}; - testSupport.reject = {}; - testSupport.promises = {}; - testSupport.waitConditionPromise = new Promise((resolve, reject) => { - testSupport.finishWaiting = resolve; - }); - - testSupport.makeTestPromise = (param) => { - testSupport.promises[param] = new Promise((resolve, reject) => { - testSupport.resolve[param] = resolve; - testSupport.reject[param] = reject; - }); - - return testSupport.promises[param]; - }; - }); - } - - val m_window; - val m_testSupport; - -private Q_SLOTS: - void simpleResolve(); - void multipleResolve(); - void simpleReject(); - void multipleReject(); - void throwInThen(); - void bareFinally(); - void finallyWithThen(); - void finallyWithThrow(); - void finallyWithThrowInThen(); - void nested(); - void all(); - void allWithThrow(); - void allWithFinally(); - void allWithFinallyAndThrow(); -}; - -tst_QStdWeb* tst_QStdWeb::instance = nullptr; - -EM_ASYNC_JS(void, awaitCondition, (), { - await testSupport.waitConditionPromise; -}); - -void tst_QStdWeb::simpleResolve() -{ - SKIP_IF_NO_ASYNCIFY(); - - init(); - - qstdweb::Promise::make(m_testSupport, "makeTestPromise", { - .thenFunc = [](val result) { - QVERIFY(result.isString()); - QCOMPARE("Some lovely data", result.as<std::string>()); - EM_ASM({ - testSupport.finishWaiting(); - }); - }, - .catchFunc = [](val error) { - Q_UNUSED(error); - QFAIL("Unexpected catch"); - } - }, std::string("simpleResolve")); - - EM_ASM({ - testSupport.resolve["simpleResolve"]("Some lovely data"); - }); - - awaitCondition(); -} - -void tst_QStdWeb::multipleResolve() -{ - SKIP_IF_NO_ASYNCIFY(); - - init(); - - qstdweb::Promise::make(m_testSupport, "makeTestPromise", { - .thenFunc = [](val result) { - QVERIFY(result.isString()); - QCOMPARE("Data 1", result.as<std::string>()); - - EM_ASM({ - if (!--testSupport.promisesLeft) { - testSupport.finishWaiting(); - } - }); - }, - .catchFunc = [](val error) { - Q_UNUSED(error); - QFAIL("Unexpected catch"); - } - }, std::string("1")); - qstdweb::Promise::make(m_testSupport, "makeTestPromise", { - .thenFunc = [](val result) { - QVERIFY(result.isString()); - QCOMPARE("Data 2", result.as<std::string>()); - - EM_ASM({ - if (!--testSupport.promisesLeft) { - testSupport.finishWaiting(); - } - }); - }, - .catchFunc = [](val error) { - Q_UNUSED(error); - QFAIL("Unexpected catch"); - } - }, std::string("2")); - qstdweb::Promise::make(m_testSupport, "makeTestPromise", { - .thenFunc = [](val result) { - QVERIFY(result.isString()); - QCOMPARE("Data 3", result.as<std::string>()); - - EM_ASM({ - if (!--testSupport.promisesLeft) { - testSupport.finishWaiting(); - } - }); - }, - .catchFunc = [](val error) { - Q_UNUSED(error); - QFAIL("Unexpected catch"); - } - }, std::string("3")); - - EM_ASM({ - testSupport.resolve["3"]("Data 3"); - testSupport.resolve["1"]("Data 1"); - testSupport.resolve["2"]("Data 2"); - }); - - awaitCondition(); -} - -void tst_QStdWeb::simpleReject() -{ - SKIP_IF_NO_ASYNCIFY(); - - init(); - - qstdweb::Promise::make(m_testSupport, "makeTestPromise", { - .thenFunc = [](val result) { - Q_UNUSED(result); - QFAIL("Unexpected then"); - }, - .catchFunc = [](val result) { - QVERIFY(result.isString()); - QCOMPARE("Evil error", result.as<std::string>()); - EM_ASM({ - testSupport.finishWaiting(); - }); - } - }, std::string("simpleReject")); - - EM_ASM({ - testSupport.reject["simpleReject"]("Evil error"); - }); - - awaitCondition(); -} - -void tst_QStdWeb::multipleReject() -{ - SKIP_IF_NO_ASYNCIFY(); - - init(); - - qstdweb::Promise::make(m_testSupport, "makeTestPromise", { - .thenFunc = [](val result) { - Q_UNUSED(result); - QFAIL("Unexpected then"); - }, - .catchFunc = [](val error) { - QVERIFY(error.isString()); - QCOMPARE("Error 1", error.as<std::string>()); - - EM_ASM({ - if (!--testSupport.promisesLeft) { - testSupport.finishWaiting(); - } - }); - } - }, std::string("1")); - qstdweb::Promise::make(m_testSupport, "makeTestPromise", { - .thenFunc = [](val result) { - Q_UNUSED(result); - QFAIL("Unexpected then"); - }, - .catchFunc = [](val error) { - QVERIFY(error.isString()); - QCOMPARE("Error 2", error.as<std::string>()); - - EM_ASM({ - if (!--testSupport.promisesLeft) { - testSupport.finishWaiting(); - } - }); - } - }, std::string("2")); - qstdweb::Promise::make(m_testSupport, "makeTestPromise", { - .thenFunc = [](val result) { - Q_UNUSED(result); - QFAIL("Unexpected then"); - }, - .catchFunc = [](val error) { - QVERIFY(error.isString()); - QCOMPARE("Error 3", error.as<std::string>()); - - EM_ASM({ - if (!--testSupport.promisesLeft) { - testSupport.finishWaiting(); - } - }); - } - }, std::string("3")); - - EM_ASM({ - testSupport.reject["3"]("Error 3"); - testSupport.reject["1"]("Error 1"); - testSupport.reject["2"]("Error 2"); - }); - - awaitCondition(); -} - -void tst_QStdWeb::throwInThen() -{ - SKIP_IF_NO_ASYNCIFY(); - - init(); - - qstdweb::Promise::make(m_testSupport, "makeTestPromise", { - .thenFunc = [](val result) { - Q_UNUSED(result); - EM_ASM({ - throw "Expected error"; - }); - }, - .catchFunc = [](val error) { - QCOMPARE("Expected error", error.as<std::string>()); - EM_ASM({ - testSupport.finishWaiting(); - }); - } - }, std::string("throwInThen")); - - EM_ASM({ - testSupport.resolve["throwInThen"](); - }); - - awaitCondition(); -} - -void tst_QStdWeb::bareFinally() -{ - SKIP_IF_NO_ASYNCIFY(); - - init(); - - qstdweb::Promise::make(m_testSupport, "makeTestPromise", { - .finallyFunc = []() { - EM_ASM({ - testSupport.finishWaiting(); - }); - } - }, std::string("bareFinally")); - - EM_ASM({ - testSupport.resolve["bareFinally"](); - }); - - awaitCondition(); -} - -void tst_QStdWeb::finallyWithThen() -{ - SKIP_IF_NO_ASYNCIFY(); - - init(); - - qstdweb::Promise::make(m_testSupport, "makeTestPromise", { - .thenFunc = [] (val result) { - Q_UNUSED(result); - }, - .finallyFunc = []() { - EM_ASM({ - testSupport.finishWaiting(); - }); - } - }, std::string("finallyWithThen")); - - EM_ASM({ - testSupport.resolve["finallyWithThen"](); - }); - - awaitCondition(); -} - -void tst_QStdWeb::finallyWithThrow() -{ - SKIP_IF_NO_ASYNCIFY(); - - init(); - - qstdweb::Promise::make(m_testSupport, "makeTestPromise", { - .catchFunc = [](val error) { - Q_UNUSED(error); - }, - .finallyFunc = []() { - EM_ASM({ - testSupport.finishWaiting(); - }); - } - }, std::string("finallyWithThrow")); - - EM_ASM({ - testSupport.reject["finallyWithThrow"](); - }); - - awaitCondition(); -} - -void tst_QStdWeb::finallyWithThrowInThen() -{ - SKIP_IF_NO_ASYNCIFY(); - - init(); - - qstdweb::Promise::make(m_testSupport, "makeTestPromise", { - .thenFunc = [](val result) { - Q_UNUSED(result); - EM_ASM({ - throw "Expected error"; - }); - }, - .catchFunc = [](val result) { - QVERIFY(result.isString()); - QCOMPARE("Expected error", result.as<std::string>()); - }, - .finallyFunc = []() { - EM_ASM({ - testSupport.finishWaiting(); - }); - } - }, std::string("bareFinallyWithThen")); - - EM_ASM({ - testSupport.resolve["bareFinallyWithThen"](); - }); - - awaitCondition(); -} - -void tst_QStdWeb::nested() -{ - SKIP_IF_NO_ASYNCIFY(); - - init(); - - qstdweb::Promise::make(m_testSupport, "makeTestPromise", { - .thenFunc = [this](val result) { - QVERIFY(result.isString()); - QCOMPARE("Outer data", result.as<std::string>()); - - qstdweb::Promise::make(m_testSupport, "makeTestPromise", { - .thenFunc = [this](val innerResult) { - QVERIFY(innerResult.isString()); - QCOMPARE("Inner data", innerResult.as<std::string>()); - - qstdweb::Promise::make(m_testSupport, "makeTestPromise", { - .thenFunc = [](val innerResult) { - QVERIFY(innerResult.isString()); - QCOMPARE("Innermost data", innerResult.as<std::string>()); - - EM_ASM({ - testSupport.finishWaiting(); - }); - }, - .catchFunc = [](val error) { - Q_UNUSED(error); - QFAIL("Unexpected catch"); - } - }, std::string("innermost")); - - EM_ASM({ - testSupport.finishWaiting(); - }); - }, - .catchFunc = [](val error) { - Q_UNUSED(error); - QFAIL("Unexpected catch"); - } - }, std::string("inner")); - - EM_ASM({ - testSupport.finishWaiting(); - }); - }, - .catchFunc = [](val error) { - Q_UNUSED(error); - QFAIL("Unexpected catch"); - } - }, std::string("outer")); - - EM_ASM({ - testSupport.resolve["outer"]("Outer data"); - }); - - awaitCondition(); - - EM_ASM({ - testSupport.waitConditionPromise = new Promise((resolve, reject) => { - testSupport.finishWaiting = resolve; - }); - }); - - EM_ASM({ - testSupport.resolve["inner"]("Inner data"); - }); - - awaitCondition(); - - EM_ASM({ - testSupport.waitConditionPromise = new Promise((resolve, reject) => { - testSupport.finishWaiting = resolve; - }); - }); - - EM_ASM({ - testSupport.resolve["innermost"]("Innermost data"); - }); - - awaitCondition(); -} - -void tst_QStdWeb::all() -{ - SKIP_IF_NO_ASYNCIFY(); - - init(); - - val promise1 = m_testSupport.call<val>("makeTestPromise", val("promise1")); - val promise2 = m_testSupport.call<val>("makeTestPromise", val("promise2")); - val promise3 = m_testSupport.call<val>("makeTestPromise", val("promise3")); - - auto thenCalledOnce = std::shared_ptr<bool>(); - *thenCalledOnce = true; - - qstdweb::Promise::all({promise1, promise2, promise3}, { - .thenFunc = [thenCalledOnce](val result) { - QVERIFY(*thenCalledOnce); - *thenCalledOnce = false; - - QVERIFY(result.isArray()); - QCOMPARE(3, result["length"].as<int>()); - QCOMPARE("Data 1", result[0].as<std::string>()); - QCOMPARE("Data 2", result[1].as<std::string>()); - QCOMPARE("Data 3", result[2].as<std::string>()); - - EM_ASM({ - testSupport.finishWaiting(); - }); - }, - .catchFunc = [](val result) { - Q_UNUSED(result); - EM_ASM({ - throw new Error("Unexpected error"); - }); - } - }); - - EM_ASM({ - testSupport.resolve["promise3"]("Data 3"); - testSupport.resolve["promise1"]("Data 1"); - testSupport.resolve["promise2"]("Data 2"); - }); - - awaitCondition(); -} - -void tst_QStdWeb::allWithThrow() -{ - SKIP_IF_NO_ASYNCIFY(); - - init(); - - val promise1 = m_testSupport.call<val>("makeTestPromise", val("promise1")); - val promise2 = m_testSupport.call<val>("makeTestPromise", val("promise2")); - val promise3 = m_testSupport.call<val>("makeTestPromise", val("promise3")); - - auto catchCalledOnce = std::shared_ptr<bool>(); - *catchCalledOnce = true; - - qstdweb::Promise::all({promise1, promise2, promise3}, { - .thenFunc = [](val result) { - Q_UNUSED(result); - QFAIL("Unexpected then"); - }, - .catchFunc = [catchCalledOnce](val result) { - QVERIFY(*catchCalledOnce); - *catchCalledOnce = false; - QVERIFY(result.isString()); - QCOMPARE("Error 2", result.as<std::string>()); - EM_ASM({ - testSupport.finishWaiting(); - }); - } - }); - - EM_ASM({ - testSupport.resolve["promise3"]("Data 3"); - testSupport.resolve["promise1"]("Data 1"); - testSupport.reject["promise2"]("Error 2"); - }); - - awaitCondition(); -} - -void tst_QStdWeb::allWithFinally() -{ - SKIP_IF_NO_ASYNCIFY(); - - init(); - - val promise1 = m_testSupport.call<val>("makeTestPromise", val("promise1")); - val promise2 = m_testSupport.call<val>("makeTestPromise", val("promise2")); - val promise3 = m_testSupport.call<val>("makeTestPromise", val("promise3")); - - auto finallyCalledOnce = std::shared_ptr<bool>(); - *finallyCalledOnce = true; - - qstdweb::Promise::all({promise1, promise2, promise3}, { - .thenFunc = [](val result) { - Q_UNUSED(result); - }, - .finallyFunc = [finallyCalledOnce]() { - QVERIFY(*finallyCalledOnce); - *finallyCalledOnce = false; - EM_ASM({ - testSupport.finishWaiting(); - }); - } - }); - - EM_ASM({ - testSupport.resolve["promise3"]("Data 3"); - testSupport.resolve["promise1"]("Data 1"); - testSupport.resolve["promise2"]("Data 2"); - }); - - awaitCondition(); -} - -void tst_QStdWeb::allWithFinallyAndThrow() -{ - SKIP_IF_NO_ASYNCIFY(); - - init(); - - val promise1 = m_testSupport.call<val>("makeTestPromise", val("promise1")); - val promise2 = m_testSupport.call<val>("makeTestPromise", val("promise2")); - val promise3 = m_testSupport.call<val>("makeTestPromise", val("promise3")); - - auto finallyCalledOnce = std::shared_ptr<bool>(); - *finallyCalledOnce = true; - - qstdweb::Promise::all({promise1, promise2, promise3}, { - .thenFunc = [](val result) { - Q_UNUSED(result); - EM_ASM({ - throw "This breaks it all!!!"; - }); - }, - .finallyFunc = [finallyCalledOnce]() { - QVERIFY(*finallyCalledOnce); - *finallyCalledOnce = false; - EM_ASM({ - testSupport.finishWaiting(); - }); - } - }); - - EM_ASM({ - testSupport.resolve["promise3"]("Data 3"); - testSupport.resolve["promise1"]("Data 1"); - testSupport.resolve["promise2"]("Data 2"); - }); - - awaitCondition(); -} - -QTEST_APPLESS_MAIN(tst_QStdWeb) -#include "tst_qstdweb.moc" |