summaryrefslogtreecommitdiffstats
path: root/tests/manual/wasm
diff options
context:
space:
mode:
Diffstat (limited to 'tests/manual/wasm')
-rw-r--r--tests/manual/wasm/CMakeLists.txt13
-rw-r--r--tests/manual/wasm/README.md15
-rw-r--r--tests/manual/wasm/a11y/CMakeLists.txt3
-rw-r--r--tests/manual/wasm/a11y/basic_widgets/CMakeLists.txt21
-rw-r--r--tests/manual/wasm/a11y/basic_widgets/basic_widgets.html24
-rw-r--r--tests/manual/wasm/a11y/basic_widgets/basica11ywidget.cpp114
-rw-r--r--tests/manual/wasm/a11y/basic_widgets/basica11ywidget.h41
-rw-r--r--tests/manual/wasm/a11y/basic_widgets/main.cpp17
-rw-r--r--tests/manual/wasm/a11y/basic_widgets/tabswidget.cpp63
-rw-r--r--tests/manual/wasm/a11y/basic_widgets/tabswidget.h34
-rw-r--r--tests/manual/wasm/clipboard/CMakeLists.txt40
-rw-r--r--tests/manual/wasm/clipboard/README2
-rw-r--r--tests/manual/wasm/clipboard/clipboard.pro27
-rw-r--r--tests/manual/wasm/clipboard/data.qrc5
-rw-r--r--tests/manual/wasm/clipboard/data/qticon64.pngbin0 -> 6474 bytes
-rw-r--r--tests/manual/wasm/clipboard/main.cpp14
-rw-r--r--tests/manual/wasm/clipboard/mainwindow.cpp314
-rw-r--r--tests/manual/wasm/clipboard/mainwindow.h52
-rw-r--r--tests/manual/wasm/clipboard/mainwindow.ui222
-rw-r--r--tests/manual/wasm/cursors/CMakeLists.txt15
-rw-r--r--tests/manual/wasm/cursors/MainWindow.cpp51
-rw-r--r--tests/manual/wasm/cursors/MainWindow.h51
-rw-r--r--tests/manual/wasm/cursors/main.cpp51
-rw-r--r--tests/manual/wasm/eventloop/CMakeLists.txt11
-rw-r--r--tests/manual/wasm/eventloop/README.md15
-rw-r--r--tests/manual/wasm/eventloop/asyncify_exec/CMakeLists.txt12
-rw-r--r--tests/manual/wasm/eventloop/asyncify_exec/main.cpp25
-rw-r--r--tests/manual/wasm/eventloop/dialog_exec/CMakeLists.txt12
-rw-r--r--tests/manual/wasm/eventloop/dialog_exec/main.cpp48
-rw-r--r--tests/manual/wasm/eventloop/eventloop_auto/CMakeLists.txt43
-rw-r--r--tests/manual/wasm/eventloop/eventloop_auto/eventloop_auto.html10
-rw-r--r--tests/manual/wasm/eventloop/eventloop_auto/eventloop_auto_asyncify.html10
-rw-r--r--tests/manual/wasm/eventloop/eventloop_auto/main.cpp327
-rw-r--r--tests/manual/wasm/eventloop/main_exec/CMakeLists.txt11
-rw-r--r--tests/manual/wasm/eventloop/main_exec/main.cpp67
-rw-r--r--tests/manual/wasm/eventloop/main_noexec/CMakeLists.txt11
-rw-r--r--tests/manual/wasm/eventloop/main_noexec/main.cpp66
-rw-r--r--tests/manual/wasm/eventloop/thread_exec/CMakeLists.txt11
-rw-r--r--tests/manual/wasm/eventloop/thread_exec/main.cpp75
-rw-r--r--tests/manual/wasm/eventloop/thread_exec/thread_exec.html0
-rw-r--r--tests/manual/wasm/localfiles/CMakeLists.txt12
-rw-r--r--tests/manual/wasm/localfiles/main.cpp228
-rw-r--r--tests/manual/wasm/localfonts/CMakeLists.txt4
-rw-r--r--tests/manual/wasm/localfonts/fontloading/CMakeLists.txt20
-rw-r--r--tests/manual/wasm/localfonts/fontloading/fontloading.html167
-rw-r--r--tests/manual/wasm/localfonts/fontloading/main.cpp78
-rw-r--r--tests/manual/wasm/network/CMakeLists.txt0
-rw-r--r--tests/manual/wasm/network/echo_client_mainthread/CMakeLists.txt8
-rw-r--r--tests/manual/wasm/network/echo_client_mainthread/main.cpp52
-rw-r--r--tests/manual/wasm/network/echo_client_secondarythread/CMakeLists.txt8
-rw-r--r--tests/manual/wasm/network/echo_client_secondarythread/main.cpp50
-rw-r--r--tests/manual/wasm/network/echo_server/CMakeLists.txt14
-rw-r--r--tests/manual/wasm/network/echo_server/main.cpp80
-rw-r--r--tests/manual/wasm/network/sockify_sockets_auto/CMakeLists.txt22
-rw-r--r--tests/manual/wasm/network/sockify_sockets_auto/main.cpp318
-rw-r--r--tests/manual/wasm/network/sockify_sockets_auto/sockify_sockets_auto.html17
-rw-r--r--tests/manual/wasm/qstdweb/CMakeLists.txt97
-rw-r--r--tests/manual/wasm/qstdweb/files_auto.html13
-rw-r--r--tests/manual/wasm/qstdweb/files_main.cpp471
-rw-r--r--tests/manual/wasm/qstdweb/iodevices_auto.html10
-rw-r--r--tests/manual/wasm/qstdweb/iodevices_main.cpp103
-rw-r--r--tests/manual/wasm/qstdweb/promise_auto.html10
-rw-r--r--tests/manual/wasm/qstdweb/promise_main.cpp486
-rw-r--r--tests/manual/wasm/qstdweb/qwasmcompositor_auto.html10
-rw-r--r--tests/manual/wasm/qstdweb/qwasmcompositor_main.cpp172
-rw-r--r--tests/manual/wasm/qtloader/tst_qtloader.html19
-rw-r--r--tests/manual/wasm/qtloader/tst_qtloader.js42
-rw-r--r--tests/manual/wasm/qtloader_integration/CMakeLists.txt45
-rw-r--r--tests/manual/wasm/qtloader_integration/main.cpp183
-rw-r--r--tests/manual/wasm/qtloader_integration/preload.json10
-rw-r--r--tests/manual/wasm/qtloader_integration/test_body.js517
-rw-r--r--tests/manual/wasm/qtloader_integration/tst_qtloader_integration.html13
-rw-r--r--tests/manual/wasm/qtwasmtestlib/README.md75
-rw-r--r--tests/manual/wasm/qtwasmtestlib/qtwasmtestlib.cpp175
-rw-r--r--tests/manual/wasm/qtwasmtestlib/qtwasmtestlib.h73
-rw-r--r--tests/manual/wasm/qtwasmtestlib/qtwasmtestlib.js137
-rw-r--r--tests/manual/wasm/rasterwindow/CMakeLists.txt11
-rw-r--r--tests/manual/wasm/rasterwindow/main.cpp14
-rw-r--r--tests/manual/wasm/rasterwindow/rasterwindow.cpp185
-rw-r--r--tests/manual/wasm/rasterwindow/rasterwindow.h43
-rw-r--r--tests/manual/wasm/shared/.gitignore1
-rwxr-xr-xtests/manual/wasm/shared/run.sh30
-rw-r--r--tests/manual/wasm/shared/testrunner.js161
83 files changed, 5885 insertions, 242 deletions
diff --git a/tests/manual/wasm/CMakeLists.txt b/tests/manual/wasm/CMakeLists.txt
new file mode 100644
index 0000000000..b13f6781b8
--- /dev/null
+++ b/tests/manual/wasm/CMakeLists.txt
@@ -0,0 +1,13 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+
+add_subdirectory(eventloop)
+add_subdirectory(rasterwindow)
+add_subdirectory(a11y)
+if(QT_FEATURE_widgets)
+add_subdirectory(cursors)
+add_subdirectory(localfiles)
+add_subdirectory(localfonts)
+add_subdirectory(qstdweb)
+add_subdirectory(clipboard)
+endif()
diff --git a/tests/manual/wasm/README.md b/tests/manual/wasm/README.md
new file mode 100644
index 0000000000..9266f38cc6
--- /dev/null
+++ b/tests/manual/wasm/README.md
@@ -0,0 +1,15 @@
+Manual tests and examples for Qt on WebAssembly
+===============================================
+
+These examples demonstrates various technical aspects of
+the Qt for WebAssambly platform port, and can be used
+as a reference when writing application code.
+
+Content
+-------
+
+ cursors Cursor handling
+ eventloop Event loops, application startup, dialog exec()
+ localfiles Local file download and upload
+ rasterwindow Basic GUI app, event handling
+ qtwasmtestlib native auto test framework
diff --git a/tests/manual/wasm/a11y/CMakeLists.txt b/tests/manual/wasm/a11y/CMakeLists.txt
new file mode 100644
index 0000000000..5268d53c8b
--- /dev/null
+++ b/tests/manual/wasm/a11y/CMakeLists.txt
@@ -0,0 +1,3 @@
+if(QT_FEATURE_widgets)
+add_subdirectory(basic_widgets)
+endif()
diff --git a/tests/manual/wasm/a11y/basic_widgets/CMakeLists.txt b/tests/manual/wasm/a11y/basic_widgets/CMakeLists.txt
new file mode 100644
index 0000000000..11534bdf68
--- /dev/null
+++ b/tests/manual/wasm/a11y/basic_widgets/CMakeLists.txt
@@ -0,0 +1,21 @@
+qt_internal_add_manual_test(a11y_basic_widgets
+ GUI
+ SOURCES
+ tabswidget.cpp
+ tabswidget.h
+ basica11ywidget.h
+ basica11ywidget.cpp
+ main.cpp
+ LIBRARIES
+ Qt::Core
+ Qt::Gui
+ Qt::Widgets
+)
+
+add_custom_command(
+ TARGET a11y_basic_widgets PRE_BUILD
+ COMMAND ${CMAKE_COMMAND} -E copy
+ ${CMAKE_CURRENT_SOURCE_DIR}/basic_widgets.html
+ ${CMAKE_CURRENT_BINARY_DIR}/basic_widgets.html
+ DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/basic_widgets.html
+)
diff --git a/tests/manual/wasm/a11y/basic_widgets/basic_widgets.html b/tests/manual/wasm/a11y/basic_widgets/basic_widgets.html
new file mode 100644
index 0000000000..091809be5c
--- /dev/null
+++ b/tests/manual/wasm/a11y/basic_widgets/basic_widgets.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+
+<script src="a11y_basic_widgets.js" async></script>
+
+<script>
+ window.onload = async () => {
+ let qt_instance = await a11y_basic_widgets_entry({
+ qtContainerElements: [document.getElementById("qt_container")],
+ });
+ }
+</script>
+
+
+</head>
+
+<body>
+ <h1>Qt Accessibility Tester</H1>
+ <div id="qt_container" style="width:640px; height:640px"></div>
+</body>
+
+</html>
diff --git a/tests/manual/wasm/a11y/basic_widgets/basica11ywidget.cpp b/tests/manual/wasm/a11y/basic_widgets/basica11ywidget.cpp
new file mode 100644
index 0000000000..dc1688f5b9
--- /dev/null
+++ b/tests/manual/wasm/a11y/basic_widgets/basica11ywidget.cpp
@@ -0,0 +1,114 @@
+// Copyright (C) 2021 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#include "basica11ywidget.h"
+
+BasicA11yWidget::BasicA11yWidget() :
+ m_toolBar (new QToolBar()),
+ m_layout(new QVBoxLayout),
+ m_tabWidget(new QTabWidget)
+{
+ createActions();
+ createMenus();
+ createToolBar();
+ m_lblDateTime =new QLabel("Select Chrono Menu for todays date and time.");
+ m_layout->addWidget(m_lblDateTime);
+ m_tabWidget->addTab(new GeneralTab(), ("General Widget"));
+ m_editView =new EditViewTab();
+ m_tabWidget->addTab(m_editView, ("Edit Widget"));
+ m_layout->addWidget(m_tabWidget);
+
+ m_layout->addStretch();
+
+ connect(m_editView, &EditViewTab::connectToToolBar, this,&BasicA11yWidget::connectToolBar);
+ setLayout(m_layout);
+
+}
+void BasicA11yWidget::handleButton() {
+
+ QDialog *asmSmplDlg = new QDialog(this);
+ QVBoxLayout *vlayout = new QVBoxLayout(asmSmplDlg);
+ asmSmplDlg->setWindowTitle("WebAssembly Dialog box ");
+ QLabel *label = new QLabel("Accessibility Demo sample application developed in Qt.");
+ QAbstractButton *bExit = new QPushButton("Exit");
+ vlayout->addWidget(label);
+ vlayout->addWidget(bExit);
+ asmSmplDlg->setLayout(vlayout);
+ auto p = asmSmplDlg->palette();
+ p.setColor( asmSmplDlg->backgroundRole(), Qt::gray);
+ asmSmplDlg->setPalette(p);
+ asmSmplDlg->show();
+ asmSmplDlg->connect(bExit, SIGNAL(clicked()), asmSmplDlg, SLOT(close()));
+}
+
+void BasicA11yWidget::createToolBar()
+{
+ m_copyAct = new QAction(tr("&Copy"), this);
+ m_copyAct->setShortcuts(QKeySequence::Copy);
+
+ m_pasteAct = new QAction(tr("&Paste"), this);
+ m_pasteAct->setStatusTip(tr("To paste selected text"));
+ m_pasteAct->setShortcuts(QKeySequence::Paste);
+
+ m_cutAct = new QAction(tr("C&ut"), this);
+ m_cutAct->setShortcuts(QKeySequence::Cut);
+
+ m_toolBar->addAction(m_copyAct);
+ m_toolBar->addAction(m_cutAct);
+ m_toolBar->addAction(m_pasteAct);
+ m_layout->addWidget(m_toolBar);
+
+}
+void BasicA11yWidget::connectToolBar()
+{
+ connect(m_copyAct, &QAction::triggered, m_editView->getTextEdit(), &QPlainTextEdit::copy);
+ connect(m_pasteAct, &QAction::triggered, m_editView->getTextEdit(), &QPlainTextEdit::paste);
+ connect(m_cutAct, &QAction::triggered, m_editView->getTextEdit(), &QPlainTextEdit::cut);
+}
+void BasicA11yWidget::createActions()
+{
+ m_DateAct = new QAction( tr("&Date"), this);
+ m_DateAct->setStatusTip(tr("To tell you todays date."));
+ connect(m_DateAct, &QAction::triggered, this, &BasicA11yWidget::todaysDate);
+
+ m_TimeAct = new QAction(tr("&Time"), this);
+ m_TimeAct->setStatusTip(tr("To tell you current time."));
+ connect(m_TimeAct, &QAction::triggered, this, &BasicA11yWidget::currentTime);
+
+}
+void BasicA11yWidget::createMenus()
+{
+ m_menuBar = new QMenuBar();
+
+ m_TodayMenu = m_menuBar->addMenu(tr("&Chrono"));
+ m_TodayMenu->addAction(m_DateAct);
+ m_TodayMenu->addAction(m_TimeAct);
+
+ m_aboutAct = new QAction(tr("&About"), this);
+ m_aboutAct->setStatusTip(tr("Show the application's About box"));
+ connect(m_aboutAct, &QAction::triggered, this, &BasicA11yWidget::about);
+
+ m_helpMenu = m_menuBar->addMenu(tr("&Help"));
+ m_helpMenu->addAction(m_aboutAct);
+
+ m_layout->setMenuBar(m_menuBar);
+}
+
+void BasicA11yWidget::todaysDate()
+{
+ QDateTime dt=QDateTime::currentDateTime();
+ QString str = "Today's Date:"+ dt.date().toString();
+ m_lblDateTime->setText(str);
+}
+
+void BasicA11yWidget::currentTime()
+{
+ QDateTime dt=QDateTime::currentDateTime();
+ QString str = "Current Time:"+ dt.time().toString();
+ m_lblDateTime->setText(str);
+}
+
+void BasicA11yWidget::about()
+{
+ handleButton();
+}
diff --git a/tests/manual/wasm/a11y/basic_widgets/basica11ywidget.h b/tests/manual/wasm/a11y/basic_widgets/basica11ywidget.h
new file mode 100644
index 0000000000..b990d163e5
--- /dev/null
+++ b/tests/manual/wasm/a11y/basic_widgets/basica11ywidget.h
@@ -0,0 +1,41 @@
+// Copyright (C) 2021 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#include <QtWidgets>
+#include "tabswidget.h"
+
+class BasicA11yWidget: public QWidget
+{
+ Q_OBJECT
+private:
+ QMenu* m_helpMenu = nullptr;
+ QMenu* m_TodayMenu = nullptr;
+ QMenuBar* m_menuBar = nullptr;
+ QToolBar* m_toolBar = nullptr;
+ QLabel* m_lblDateTime = nullptr;
+ QVBoxLayout* m_layout = nullptr ;
+ QTabWidget* m_tabWidget = nullptr;
+ EditViewTab *m_editView = nullptr;
+
+ QAction* m_DateAct = nullptr;
+ QAction* m_TimeAct = nullptr;
+ QAction* m_aboutAct = nullptr;
+ QAction* m_copyAct = nullptr;
+ QAction* m_pasteAct = nullptr;
+ QAction* m_cutAct = nullptr;
+
+public slots:
+ void connectToolBar();
+public:
+ BasicA11yWidget() ;
+ void createActions();
+ void createMenus();
+ void createToolBar();
+
+ void todaysDate();
+ void currentTime();
+ void about();
+ QToolBar* getToolbar(){return m_toolBar;}
+ void handleButton();
+
+};
diff --git a/tests/manual/wasm/a11y/basic_widgets/main.cpp b/tests/manual/wasm/a11y/basic_widgets/main.cpp
new file mode 100644
index 0000000000..52d72428bb
--- /dev/null
+++ b/tests/manual/wasm/a11y/basic_widgets/main.cpp
@@ -0,0 +1,17 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#include <QApplication>
+#include <QtWidgets>
+#include "basica11ywidget.h"
+
+int main(int argc, char **argv)
+{
+ QApplication app(argc, argv);
+
+ BasicA11yWidget a11yWidget;
+ a11yWidget.show();
+
+ return app.exec();
+}
+
diff --git a/tests/manual/wasm/a11y/basic_widgets/tabswidget.cpp b/tests/manual/wasm/a11y/basic_widgets/tabswidget.cpp
new file mode 100644
index 0000000000..63428c965a
--- /dev/null
+++ b/tests/manual/wasm/a11y/basic_widgets/tabswidget.cpp
@@ -0,0 +1,63 @@
+// Copyright (C) 2021 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#include "tabswidget.h"
+
+GeneralTab::GeneralTab(QWidget *parent)
+ : QWidget(parent)
+{
+ QVBoxLayout *layout = new QVBoxLayout();
+ layout->setSizeConstraint(QLayout::SetMaximumSize);
+
+ layout->addWidget(new QLabel("This is a text label"));
+
+ QPushButton *btn = new QPushButton("This is a push button");
+ layout->addWidget(btn);
+ connect(btn, &QPushButton::released, this, [=] () {
+ btn->setText("You clicked me");
+ });
+
+ layout->addWidget(new QCheckBox("This is a check box"));
+
+ layout->addWidget(new QRadioButton("Radio 1"));
+ layout->addWidget(new QRadioButton("Radio 2"));
+
+ QSlider *slider = new QSlider(Qt::Horizontal);
+ slider->setTickInterval(10);
+ slider->setTickPosition(QSlider::TicksAbove);
+ layout->addWidget(slider);
+
+ QSpinBox *spin = new QSpinBox();
+ spin->setValue(10);
+ spin->setSingleStep(1);
+ layout->addWidget(spin);
+ layout->addStretch();
+
+ QScrollBar *scrollBar = new QScrollBar(Qt::Horizontal);
+ scrollBar->setFocusPolicy(Qt::StrongFocus);
+ layout->addWidget(scrollBar);
+
+ setLayout(layout);
+}
+
+
+EditViewTab::EditViewTab(QWidget *parent) :
+ QWidget(parent)
+{
+ QVBoxLayout *layout = new QVBoxLayout();
+ layout->setSizeConstraint(QLayout::SetMaximumSize);
+ textEdit = new QPlainTextEdit();
+ textEdit->setPlaceholderText("Enter Text here");
+ layout->addWidget(textEdit);
+ setLayout(layout);
+
+}
+
+void EditViewTab::showEvent( QShowEvent* event ) {
+ if (!b_connected)
+ {
+ emit connectToToolBar();
+ b_connected=true;
+ }
+ QWidget::showEvent( event );
+}
diff --git a/tests/manual/wasm/a11y/basic_widgets/tabswidget.h b/tests/manual/wasm/a11y/basic_widgets/tabswidget.h
new file mode 100644
index 0000000000..6405c0dab7
--- /dev/null
+++ b/tests/manual/wasm/a11y/basic_widgets/tabswidget.h
@@ -0,0 +1,34 @@
+// Copyright (C) 2021 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#ifndef TABDIALOG_H
+#define TABDIALOG_H
+#include <QTabWidget>
+#include <QtWidgets>
+
+class GeneralTab : public QWidget
+{
+ Q_OBJECT
+
+public:
+
+ explicit GeneralTab(QWidget *parent = nullptr);
+};
+
+class EditViewTab : public QWidget
+{
+
+ Q_OBJECT
+private:
+ bool b_connected = false;
+ QPlainTextEdit* textEdit =nullptr;
+ QToolBar* m_toolbar= nullptr;
+public:
+ void showEvent( QShowEvent* event ) ;
+ QPlainTextEdit* getTextEdit(){return textEdit;}
+ explicit EditViewTab( QWidget *parent = nullptr);
+signals:
+ void connectToToolBar();
+};
+
+#endif
diff --git a/tests/manual/wasm/clipboard/CMakeLists.txt b/tests/manual/wasm/clipboard/CMakeLists.txt
new file mode 100644
index 0000000000..40fb8ca308
--- /dev/null
+++ b/tests/manual/wasm/clipboard/CMakeLists.txt
@@ -0,0 +1,40 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+
+#####################################################################
+## clipboard Binary:
+#####################################################################
+
+qt_internal_add_manual_test(clipboard
+ GUI
+ SOURCES
+ main.cpp
+ mainwindow.cpp mainwindow.h mainwindow.ui
+ NO_PCH_SOURCES
+ main.cpp # undef QT_NO_FOREACH
+ LIBRARIES
+ Qt::Core
+ Qt::Gui
+ Qt::Widgets
+ ENABLE_AUTOGEN_TOOLS
+ uic
+)
+# Resources:
+set(data_resource_files
+ "data/qticon64.png"
+)
+
+qt_internal_add_resource(clipboard "data"
+ PREFIX
+ "/"
+ FILES
+ ${data_resource_files}
+)
+
+## Scopes:
+#####################################################################
+
+qt_internal_extend_target(clipboard CONDITION (QT_MAJOR_VERSION GREATER 4)
+ LIBRARIES
+ Qt::Widgets
+)
diff --git a/tests/manual/wasm/clipboard/README b/tests/manual/wasm/clipboard/README
new file mode 100644
index 0000000000..91529696ca
--- /dev/null
+++ b/tests/manual/wasm/clipboard/README
@@ -0,0 +1,2 @@
+The Clipboard manual test app can be used both on desktop and in the browser
+using WebAssembly to test clipboard use between WebAssembly app and the desktop.
diff --git a/tests/manual/wasm/clipboard/clipboard.pro b/tests/manual/wasm/clipboard/clipboard.pro
new file mode 100644
index 0000000000..cffce46997
--- /dev/null
+++ b/tests/manual/wasm/clipboard/clipboard.pro
@@ -0,0 +1,27 @@
+QT += core gui
+
+greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
+
+CONFIG += c++11
+
+# You can make your code fail to compile if it uses deprecated APIs.
+# In order to do so, uncomment the following line.
+#DEFINES += QT_DISABLE_DEPRECATED_UP_TO=0x060000 # disables all APIs deprecated in Qt 6.0.0 and earlier
+
+SOURCES += \
+ main.cpp \
+ mainwindow.cpp
+
+HEADERS += \
+ mainwindow.h
+
+FORMS += \
+ mainwindow.ui
+
+RESOURCES += \
+ data.qrc
+
+# Default rules for deployment.
+qnx: target.path = /tmp/$${TARGET}/bin
+else: unix:!android: target.path = /opt/$${TARGET}/bin
+!isEmpty(target.path): INSTALLS += target
diff --git a/tests/manual/wasm/clipboard/data.qrc b/tests/manual/wasm/clipboard/data.qrc
new file mode 100644
index 0000000000..c0f33f25be
--- /dev/null
+++ b/tests/manual/wasm/clipboard/data.qrc
@@ -0,0 +1,5 @@
+<RCC>
+ <qresource prefix="/">
+ <file>data/qticon64.png</file>
+ </qresource>
+</RCC>
diff --git a/tests/manual/wasm/clipboard/data/qticon64.png b/tests/manual/wasm/clipboard/data/qticon64.png
new file mode 100644
index 0000000000..76f02c6c96
--- /dev/null
+++ b/tests/manual/wasm/clipboard/data/qticon64.png
Binary files differ
diff --git a/tests/manual/wasm/clipboard/main.cpp b/tests/manual/wasm/clipboard/main.cpp
new file mode 100644
index 0000000000..aa838f6670
--- /dev/null
+++ b/tests/manual/wasm/clipboard/main.cpp
@@ -0,0 +1,14 @@
+// Copyright (C) 2021 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#include "mainwindow.h"
+
+#include <QApplication>
+
+int main(int argc, char *argv[])
+{
+ QApplication a(argc, argv);
+ MainWindow w;
+ w.show();
+ return a.exec();
+}
diff --git a/tests/manual/wasm/clipboard/mainwindow.cpp b/tests/manual/wasm/clipboard/mainwindow.cpp
new file mode 100644
index 0000000000..81a95c4218
--- /dev/null
+++ b/tests/manual/wasm/clipboard/mainwindow.cpp
@@ -0,0 +1,314 @@
+// Copyright (C) 2021 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#undef QT_NO_FOREACH // this file contains unported legacy Q_FOREACH uses
+
+#include "mainwindow.h"
+#include "ui_mainwindow.h"
+#include <QClipboard>
+#include <QMimeData>
+#include <QImageReader>
+#include <QBuffer>
+#include <QRandomGenerator>
+#include <QPainter>
+#include <QKeyEvent>
+#include <QMimeDatabase>
+#include <QFileInfo>
+#include <QCryptographicHash>
+
+#ifdef Q_OS_WASM
+#include <emscripten.h>
+#include <emscripten/html5.h>
+#include <emscripten/val.h>
+#include <emscripten/bind.h>
+
+using namespace emscripten;
+#endif
+
+MainWindow::MainWindow(QWidget *parent)
+ : QMainWindow(parent)
+ , ui(new Ui::MainWindow)
+{
+ ui->setupUi(this);
+
+ ui->imageLabel->installEventFilter(this);
+
+ ui->imageLabel->setBackgroundRole(QPalette::Base);
+ ui->imageLabel->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
+ ui->imageLabel->setScaledContents(true);
+
+ setAcceptDrops(true);
+
+ clipboard = QGuiApplication::clipboard();
+ connect(
+ clipboard, &QClipboard::dataChanged,
+ [=]() {
+ ui->textEdit_2->insertHtml("<b>Clipboard data changed:</b><br>");
+ const QMimeData *mimeData = clipboard->mimeData();
+ QByteArray ba;
+
+ for (auto mimetype : mimeData->formats()) {
+ qDebug() << Q_FUNC_INFO << mimetype;
+ ba = mimeData->data(mimetype);
+ }
+ QString sizeStr;
+
+ if (mimeData->hasImage()) {
+ qsizetype imageSize = qvariant_cast<QImage>(mimeData->imageData()).sizeInBytes();
+ sizeStr.setNum(imageSize);
+ ui->textEdit_2->insertHtml("has Image data: " + sizeStr + "<br>");
+ }
+
+ if (mimeData->hasHtml()) {
+ int size = mimeData->html().length();
+ sizeStr.setNum(size);
+ ui->textEdit_2->insertHtml("has html data: " + sizeStr + "<br>");
+ }
+ if (mimeData->hasText()) {
+ int size = mimeData->text().length();
+ sizeStr.setNum(size);
+ ui->textEdit_2->insertHtml("has text data: " + sizeStr + "<br>");
+ }
+
+ ui->textEdit_2->insertHtml(mimeData->formats().join(" | ")+ "<br>");
+
+ ui->textEdit_2->ensureCursorVisible();
+
+ const QString message = tr("Clipboard changed, %1 ")
+ .arg(mimeData->formats().join(' '));
+
+ statusBar()->showMessage(message + sizeStr);
+ }
+ );
+#ifdef Q_OS_WASM
+ val clipboard = val::global("navigator")["clipboard"];
+ bool hasClipboardApi = (!clipboard.isUndefined() && !clipboard["readText"].isUndefined());
+ QString messageApi;
+ if (hasClipboardApi)
+ messageApi = QStringLiteral("Using Clipboard API");
+ else
+ messageApi = QStringLiteral("Using Clipboard events");
+ ui->label->setText(messageApi);
+#else
+ ui->label->setText("desktop clipboard");
+#endif
+}
+
+MainWindow::~MainWindow()
+{
+ delete ui;
+}
+
+void MainWindow::on_setTextButton_clicked()
+{
+ QGuiApplication::clipboard()->setText(ui->textEdit->textCursor().selectedText());
+}
+
+static QImage clipboardImage()
+{
+ if (const QMimeData *mimeData = QGuiApplication::clipboard()->mimeData()) {
+ if (mimeData->hasImage()) {
+ const QImage image = qvariant_cast<QImage>(mimeData->imageData());
+ if (!image.isNull())
+ return image;
+ }
+ }
+ return QImage();
+}
+
+static QByteArray clipboardBinary()
+{
+ if (const QMimeData *mimeData = QGuiApplication::clipboard()->mimeData()) {
+
+ if (mimeData->formats().contains("application/octet-stream")) {
+ const QByteArray ba = qvariant_cast<QByteArray>(mimeData->data("application/octet-stream"));
+ qDebug() << Q_FUNC_INFO << ba;
+ if (!ba.isNull())
+ return ba;
+ }
+ }
+ return QByteArray();
+}
+
+void MainWindow::on_pasteImageButton_clicked()
+{
+ const QImage newImage = clipboardImage();
+ if (newImage.isNull()) {
+ qDebug() << "No image in clipboard";
+ const QString message = tr("No image in clipboard")
+ .arg(newImage.width()).arg(newImage.height()).arg(newImage.depth());
+ statusBar()->showMessage(message);
+ } else {
+ setImage(newImage);
+ setWindowFilePath(QString());
+ const QString message = tr("Obtained image from clipboard, %1x%2, Depth: %3")
+ .arg(newImage.width()).arg(newImage.height()).arg(newImage.depth());
+ statusBar()->showMessage(message);
+ }
+}
+
+void MainWindow::setImage(const QImage &newImage)
+{
+ image = newImage;
+ ui->imageLabel->setPixmap(QPixmap::fromImage(image));
+}
+
+void MainWindow::on_pasteTextButton_clicked()
+{
+ ui->textEdit->insertPlainText(QGuiApplication::clipboard()->text());
+}
+
+void MainWindow::on_copyBinaryButton_clicked()
+{
+ QByteArray ba;
+ ba.resize(10);
+ ba[0] = 0x3c;
+ ba[1] = 0xb8;
+ ba[2] = 0x64;
+ ba[3] = 0x18;
+ ba[4] = 0xca;
+ ba[5] = 0xca;
+ ba[6] = 0x18;
+ ba[7] = 0x64;
+ ba[8] = 0xb8;
+ ba[9] = 0x3c;
+
+ QMimeData *mimeData = new QMimeData();
+ mimeData->setData("application/octet-stream", ba);
+ QGuiApplication::clipboard()->setMimeData(mimeData);
+
+ const QString message = tr("Copied binary to clipboard: " + ba + " 10 bytes");
+ statusBar()->showMessage(message);
+}
+
+void MainWindow::on_pasteBinaryButton_clicked()
+{
+ const QByteArray ba = clipboardBinary();
+ if (ba.isNull()) {
+ qDebug() << "No binary in clipboard";
+ const QString message = tr("No binary in clipboard");
+ statusBar()->showMessage(message);
+ } else {
+ setWindowFilePath(QString());
+ const QString message = tr("Obtained binary from clipboard: " + ba);
+ statusBar()->showMessage(message);
+ }
+}
+
+void MainWindow::on_comboBox_textActivated(const QString &arg1)
+{
+ QImage image(QSize(150,100), QImage::Format_RGB32);
+ QPainter painter(&image);
+ painter.fillRect(QRectF(0,0,150,100),generateRandomColor());
+ painter.fillRect(QRectF(20,30,130,40),generateRandomColor());
+ painter.setPen(QPen(generateRandomColor()));
+ painter.drawText(QRect(25,30,130,40),"Qt WebAssembly");
+
+ QByteArray ba;
+ QBuffer buffer(&ba);
+ buffer.open(QIODevice::WriteOnly);
+ image.save(&buffer, arg1.toLocal8Bit());
+
+ qDebug() << ba.mid(0,10) << ba.length();
+ qDebug() << Q_FUNC_INFO << image.sizeInBytes();
+
+ QGuiApplication::clipboard()->setImage(image);
+}
+
+QColor MainWindow::generateRandomColor()
+{
+ return QColor::fromRgb(QRandomGenerator::global()->generate());
+}
+
+bool MainWindow::eventFilter(QObject *obj, QEvent *event)
+{
+ if (event->type() == QEvent::KeyPress) {
+ QKeyEvent *ke = static_cast<QKeyEvent *>(event);
+ if (ke->key() == Qt::Key_V && ke->modifiers().testFlag(Qt::ControlModifier)) {
+ if (obj == ui->imageLabel) {
+ setImage(clipboardImage());
+ return true;
+ }
+ }
+ }
+ // standard event processing
+ return QObject::eventFilter(obj, event);
+}
+
+void MainWindow::on_pasteHtmlButton_clicked()
+{
+ ui->textEdit->insertHtml(QGuiApplication::clipboard()->mimeData()->html());
+}
+
+void MainWindow::on_clearButton_clicked()
+{
+ ui->textEdit_2->clear();
+ ui->imageLabel->clear();
+ ui->imageLabel->setText("Paste or drop image here");
+}
+
+void MainWindow::dragEnterEvent(QDragEnterEvent* e)
+{
+ e->acceptProposedAction();
+}
+
+void MainWindow::dropEvent(QDropEvent* e)
+{
+ QString sizeStr;
+ ui->textEdit_2->insertPlainText("New Drop has mime formats: " + e->mimeData()->formats().join(", ") + "\n");
+
+ QString urlMessage = QString(" Drop contains %1 urls\n").arg(e->mimeData()->urls().count());
+ ui->textEdit_2->insertPlainText(urlMessage);
+
+ foreach (const QUrl &url, e->mimeData()->urls()) {
+
+ QString urlStr = url.toDisplayString();
+ int size = urlStr.length();
+ sizeStr.setNum(size);
+
+ QString fileName = url.toLocalFile();
+ QString sha1;
+ QFile file(fileName);
+ if (file.exists()) {
+ file.open(QFile::ReadOnly);
+ sha1 = QCryptographicHash::hash(file.readAll(), QCryptographicHash::Sha1).toHex();
+ }
+
+ ui->textEdit_2->insertPlainText(" Drop has url data length: " + sizeStr + "\n");
+ ui->textEdit_2->insertPlainText(" " + urlStr + " sha1 " + sha1.left(8) + "\n");
+ }
+ ui->textEdit_2->insertPlainText("\n");
+
+ if (e->mimeData()->hasImage()) {
+ qsizetype imageSize = qvariant_cast<QImage>(e->mimeData()->imageData()).sizeInBytes();
+ sizeStr.setNum(imageSize);
+ ui->textEdit_2->insertPlainText(" Drop has Image data length: " + sizeStr + "\n");
+ QImage image = qvariant_cast<QImage>(e->mimeData()->imageData());
+ setImage(image);
+ const QString message = tr("Obtained image from drop, %1x%2, Depth: %3")
+ .arg(image.width()).arg(image.height()).arg(image.depth());
+ statusBar()->showMessage(message);
+ }
+
+ if (e->mimeData()->hasHtml()) {
+ int size = e->mimeData()->html().length();
+ sizeStr.setNum(size);
+ ui->textEdit_2->insertPlainText(" Drop has html data length: " + sizeStr + "\n");
+ for (const auto &line : e->mimeData()->html().split('\n', Qt::SkipEmptyParts))
+ ui->textEdit_2->insertPlainText(" " + line + "\n");
+ }
+ if (e->mimeData()->hasText()) {
+ int size = e->mimeData()->text().length();
+ sizeStr.setNum(size);
+ ui->textEdit_2->insertPlainText(" Drop has text data length: " + sizeStr + "\n");
+ for (const auto &line : e->mimeData()->text().split('\n', Qt::SkipEmptyParts))
+ ui->textEdit_2->insertPlainText(" " + line + "\n");
+ }
+
+ const QString message = tr(" Drop accepted, %1 ")
+ .arg(e->mimeData()->formats().join(' '));
+
+ statusBar()->showMessage(message + sizeStr);
+
+ e->acceptProposedAction();
+}
diff --git a/tests/manual/wasm/clipboard/mainwindow.h b/tests/manual/wasm/clipboard/mainwindow.h
new file mode 100644
index 0000000000..fe101ad494
--- /dev/null
+++ b/tests/manual/wasm/clipboard/mainwindow.h
@@ -0,0 +1,52 @@
+// Copyright (C) 2021 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+
+QT_BEGIN_NAMESPACE
+namespace Ui { class MainWindow; }
+QT_END_NAMESPACE
+
+class MainWindow : public QMainWindow
+{
+ Q_OBJECT
+
+public:
+ MainWindow(QWidget *parent = nullptr);
+ ~MainWindow();
+
+private slots:
+ void on_setTextButton_clicked();
+
+ void on_pasteImageButton_clicked();
+ void setImage(const QImage &newImage);
+ void on_pasteTextButton_clicked();
+
+
+ void on_copyBinaryButton_clicked();
+
+ void on_pasteBinaryButton_clicked();
+
+ void on_comboBox_textActivated(const QString &arg1);
+
+ void on_pasteHtmlButton_clicked();
+
+ void on_clearButton_clicked();
+
+private:
+ Ui::MainWindow *ui;
+ QImage image;
+ QClipboard *clipboard;
+ bool eventFilter(QObject *obj, QEvent *event) override;
+
+ QColor generateRandomColor();
+
+protected:
+ void dragEnterEvent(QDragEnterEvent *e) override;
+ void dropEvent(QDropEvent *e) override;
+
+};
+#endif // MAINWINDOW_H
diff --git a/tests/manual/wasm/clipboard/mainwindow.ui b/tests/manual/wasm/clipboard/mainwindow.ui
new file mode 100644
index 0000000000..17368fdc68
--- /dev/null
+++ b/tests/manual/wasm/clipboard/mainwindow.ui
@@ -0,0 +1,222 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>MainWindow</class>
+ <widget class="QMainWindow" name="MainWindow">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>1222</width>
+ <height>1011</height>
+ </rect>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Minimum" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="windowTitle">
+ <string>MainWindow</string>
+ </property>
+ <widget class="QWidget" name="centralwidget">
+ <layout class="QGridLayout" name="gridLayout_2">
+ <item row="0" column="0">
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <widget class="QPushButton" name="setTextButton">
+ <property name="text">
+ <string>setText()</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="pasteTextButton">
+ <property name="text">
+ <string>paste text</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="pasteHtmlButton">
+ <property name="text">
+ <string>paste html</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <item>
+ <widget class="QComboBox" name="comboBox">
+ <property name="currentText">
+ <string>PNG</string>
+ </property>
+ <item>
+ <property name="text">
+ <string>PNG</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>JPG</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>BMP</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>NAN</string>
+ </property>
+ </item>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="pasteImageButton">
+ <property name="text">
+ <string>paste image</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_3">
+ <item>
+ <widget class="QPushButton" name="copyBinaryButton">
+ <property name="text">
+ <string>setData</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="pasteBinaryButton">
+ <property name="text">
+ <string>paste data</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QPushButton" name="clearButton">
+ <property name="text">
+ <string>clear</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QTextEdit" name="textEdit_2">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Minimum" vsizetype="MinimumExpanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item row="0" column="1">
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="2" column="0">
+ <widget class="QTextBrowser" name="textEdit">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>500</width>
+ <height>400</height>
+ </size>
+ </property>
+ <property name="readOnly">
+ <bool>false</bool>
+ </property>
+ <property name="html">
+ <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
+&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;meta charset=&quot;utf-8&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
+p, li { white-space: pre-wrap; }
+&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'.AppleSystemUIFont'; font-size:13pt; font-weight:400; font-style:normal;&quot;&gt;
+&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;img src=&quot;:/data/qticon64.png&quot; /&gt;&lt;/p&gt;
+&lt;p style=&quot; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;a name=&quot;tw-target&quot;&gt;&lt;/a&gt;&lt;span style=&quot; font-family:'monospace'; font-size:9pt; font-weight:600;&quot;&gt;L&lt;/span&gt;&lt;span style=&quot; font-family:'monospace'; font-size:9pt; font-weight:600;&quot;&gt;orem&lt;/span&gt;&lt;span style=&quot; font-family:'monospace'; font-size:9pt;&quot;&gt; &lt;/span&gt;&lt;span style=&quot; font-family:'monospace'; font-size:9pt; font-style:italic;&quot;&gt;ipsum&lt;/span&gt;&lt;span style=&quot; font-family:'monospace'; font-size:9pt;&quot;&gt; &lt;/span&gt;&lt;span style=&quot; font-family:'monospace'; font-size:9pt; text-decoration: underline;&quot;&gt;dolor&lt;/span&gt;&lt;span style=&quot; font-family:'monospace'; font-size:9pt;&quot;&gt; &lt;/span&gt;&lt;span style=&quot; font-family:'monospace'; font-size:9pt; vertical-align:super;&quot;&gt;sit&lt;/span&gt;&lt;span style=&quot; font-family:'monospace'; font-size:9pt;&quot;&gt; &lt;/span&gt;&lt;span style=&quot; font-family:'monospace'; font-size:9pt; vertical-align:sub;&quot;&gt;amet&lt;/span&gt;&lt;span style=&quot; font-family:'monospace'; font-size:9pt;&quot;&gt;, &lt;/span&gt;&lt;a href=&quot;http://localhost&quot;&gt;&lt;span style=&quot; font-family:'Sans Serif'; font-size:9pt; text-decoration: underline; color:#0000ff;&quot;&gt;consectetur&lt;/span&gt;&lt;/a&gt;&lt;span style=&quot; font-family:'monospace'; font-size:9pt;&quot;&gt; &lt;/span&gt;&lt;span style=&quot; font-family:'monospace'; font-size:9pt; color:#7320a4;&quot;&gt;adipiscing&lt;/span&gt;&lt;span style=&quot; font-family:'monospace'; font-size:9pt;&quot;&gt; elit. Som medlemmer av byrået ønsker imidlertid en eiendomsmegler. Ullamcorper største lekseforfatter. Dolor et consectetuer litt ernæring. Maecenas smile jord sitter Vulputate medlemmer og, basketball ethvert problem. Reservert lever nå propaganda. På makroen investere laoreet kan, av enhver latter. Jasmine som en TV -tegneserie.&lt;/span&gt;&lt;/p&gt;
+&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Sans Serif'; font-size:9pt;&quot;&gt;&lt;br /&gt;&lt;/p&gt;
+&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'monospace'; font-size:9pt;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="0">
+ <widget class="QLabel" name="imageLabel">
+ <property name="mouseTracking">
+ <bool>true</bool>
+ </property>
+ <property name="focusPolicy">
+ <enum>Qt::StrongFocus</enum>
+ </property>
+ <property name="acceptDrops">
+ <bool>true</bool>
+ </property>
+ <property name="frameShape">
+ <enum>QFrame::StyledPanel</enum>
+ </property>
+ <property name="frameShadow">
+ <enum>QFrame::Raised</enum>
+ </property>
+ <property name="text">
+ <string>Paste or drop content here</string>
+ </property>
+ <property name="scaledContents">
+ <bool>true</bool>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="label">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="frameShape">
+ <enum>QFrame::Panel</enum>
+ </property>
+ <property name="frameShadow">
+ <enum>QFrame::Sunken</enum>
+ </property>
+ <property name="text">
+ <string>TextLabel</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QMenuBar" name="menubar">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>1222</width>
+ <height>24</height>
+ </rect>
+ </property>
+ </widget>
+ <widget class="QStatusBar" name="statusbar"/>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/tests/manual/wasm/cursors/CMakeLists.txt b/tests/manual/wasm/cursors/CMakeLists.txt
new file mode 100644
index 0000000000..93f93064cf
--- /dev/null
+++ b/tests/manual/wasm/cursors/CMakeLists.txt
@@ -0,0 +1,15 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+
+qt_internal_add_manual_test(cursors
+ GUI
+ SOURCES
+ MainWindow.cpp MainWindow.h MainWindow.ui
+ main.cpp
+ LIBRARIES
+ Qt::Core
+ Qt::Gui
+ Qt::Widgets
+ ENABLE_AUTOGEN_TOOLS
+ uic
+)
diff --git a/tests/manual/wasm/cursors/MainWindow.cpp b/tests/manual/wasm/cursors/MainWindow.cpp
index a850856ab3..c6e4fbcca1 100644
--- a/tests/manual/wasm/cursors/MainWindow.cpp
+++ b/tests/manual/wasm/cursors/MainWindow.cpp
@@ -1,52 +1,5 @@
-/****************************************************************************
-**
-** Copyright (C) 2019 The Qt Company Ltd.
-** Contact: https://www.qt.io/licensing/
-**
-** This file is part of the examples of the Qt Toolkit.
-**
-** $QT_BEGIN_LICENSE:BSD$
-** Commercial License Usage
-** Licensees holding valid commercial Qt licenses may use this file in
-** accordance with the commercial license agreement provided with the
-** Software or, alternatively, in accordance with the terms contained in
-** a written agreement between you and The Qt Company. For licensing terms
-** and conditions see https://www.qt.io/terms-conditions. For further
-** information use the contact form at https://www.qt.io/contact-us.
-**
-** BSD License Usage
-** Alternatively, you may use this file under the terms of the BSD license
-** as follows:
-**
-** "Redistribution and use in source and binary forms, with or without
-** modification, are permitted provided that the following conditions are
-** met:
-** * Redistributions of source code must retain the above copyright
-** notice, this list of conditions and the following disclaimer.
-** * Redistributions in binary form must reproduce the above copyright
-** notice, this list of conditions and the following disclaimer in
-** the documentation and/or other materials provided with the
-** distribution.
-** * Neither the name of The Qt Company Ltd nor the names of its
-** contributors may be used to endorse or promote products derived
-** from this software without specific prior written permission.
-**
-**
-** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
-**
-** $QT_END_LICENSE$
-**
-****************************************************************************/
+// Copyright (C) 2019 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include "MainWindow.h"
#include "ui_MainWindow.h"
diff --git a/tests/manual/wasm/cursors/MainWindow.h b/tests/manual/wasm/cursors/MainWindow.h
index cda4ef19ba..ebaeed9e5c 100644
--- a/tests/manual/wasm/cursors/MainWindow.h
+++ b/tests/manual/wasm/cursors/MainWindow.h
@@ -1,52 +1,5 @@
-/****************************************************************************
-**
-** Copyright (C) 2019 The Qt Company Ltd.
-** Contact: https://www.qt.io/licensing/
-**
-** This file is part of the examples of the Qt Toolkit.
-**
-** $QT_BEGIN_LICENSE:BSD$
-** Commercial License Usage
-** Licensees holding valid commercial Qt licenses may use this file in
-** accordance with the commercial license agreement provided with the
-** Software or, alternatively, in accordance with the terms contained in
-** a written agreement between you and The Qt Company. For licensing terms
-** and conditions see https://www.qt.io/terms-conditions. For further
-** information use the contact form at https://www.qt.io/contact-us.
-**
-** BSD License Usage
-** Alternatively, you may use this file under the terms of the BSD license
-** as follows:
-**
-** "Redistribution and use in source and binary forms, with or without
-** modification, are permitted provided that the following conditions are
-** met:
-** * Redistributions of source code must retain the above copyright
-** notice, this list of conditions and the following disclaimer.
-** * Redistributions in binary form must reproduce the above copyright
-** notice, this list of conditions and the following disclaimer in
-** the documentation and/or other materials provided with the
-** distribution.
-** * Neither the name of The Qt Company Ltd nor the names of its
-** contributors may be used to endorse or promote products derived
-** from this software without specific prior written permission.
-**
-**
-** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
-**
-** $QT_END_LICENSE$
-**
-****************************************************************************/
+// Copyright (C) 2019 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#pragma once
#include <QMainWindow>
diff --git a/tests/manual/wasm/cursors/main.cpp b/tests/manual/wasm/cursors/main.cpp
index c0b90f2c37..9a59cdd994 100644
--- a/tests/manual/wasm/cursors/main.cpp
+++ b/tests/manual/wasm/cursors/main.cpp
@@ -1,52 +1,5 @@
-/****************************************************************************
-**
-** Copyright (C) 2019 The Qt Company Ltd.
-** Contact: https://www.qt.io/licensing/
-**
-** This file is part of the examples of the Qt Toolkit.
-**
-** $QT_BEGIN_LICENSE:BSD$
-** Commercial License Usage
-** Licensees holding valid commercial Qt licenses may use this file in
-** accordance with the commercial license agreement provided with the
-** Software or, alternatively, in accordance with the terms contained in
-** a written agreement between you and The Qt Company. For licensing terms
-** and conditions see https://www.qt.io/terms-conditions. For further
-** information use the contact form at https://www.qt.io/contact-us.
-**
-** BSD License Usage
-** Alternatively, you may use this file under the terms of the BSD license
-** as follows:
-**
-** "Redistribution and use in source and binary forms, with or without
-** modification, are permitted provided that the following conditions are
-** met:
-** * Redistributions of source code must retain the above copyright
-** notice, this list of conditions and the following disclaimer.
-** * Redistributions in binary form must reproduce the above copyright
-** notice, this list of conditions and the following disclaimer in
-** the documentation and/or other materials provided with the
-** distribution.
-** * Neither the name of The Qt Company Ltd nor the names of its
-** contributors may be used to endorse or promote products derived
-** from this software without specific prior written permission.
-**
-**
-** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
-**
-** $QT_END_LICENSE$
-**
-****************************************************************************/
+// Copyright (C) 2019 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include "MainWindow.h"
#include <QApplication>
diff --git a/tests/manual/wasm/eventloop/CMakeLists.txt b/tests/manual/wasm/eventloop/CMakeLists.txt
new file mode 100644
index 0000000000..132fd15dbb
--- /dev/null
+++ b/tests/manual/wasm/eventloop/CMakeLists.txt
@@ -0,0 +1,11 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+
+add_subdirectory(asyncify_exec)
+add_subdirectory(eventloop_auto)
+add_subdirectory(main_exec)
+add_subdirectory(main_noexec)
+add_subdirectory(thread_exec)
+if(QT_FEATURE_widgets)
+add_subdirectory(dialog_exec)
+endif()
diff --git a/tests/manual/wasm/eventloop/README.md b/tests/manual/wasm/eventloop/README.md
new file mode 100644
index 0000000000..e1a5a1a3b7
--- /dev/null
+++ b/tests/manual/wasm/eventloop/README.md
@@ -0,0 +1,15 @@
+Event loop exec() and main() on Qt for WebAssembly
+==================================================
+
+These examples demonstrate how QEventLoop::exec() works on
+Qt for WebAssembly, and also shows how to implement main()
+without calling QApplication::exec().
+
+Contents
+========
+
+ main_exec Standard Qt main(), where QApplication::exec() does not return
+ main_noexec Qt main() without QApplication::exec()
+ dialog_exec Shows how QDialog::exec() also does not return
+ thread_exec Shows how to use QThread::exec()
+ eventloop_auto Event loop autotest (manually run)
diff --git a/tests/manual/wasm/eventloop/asyncify_exec/CMakeLists.txt b/tests/manual/wasm/eventloop/asyncify_exec/CMakeLists.txt
new file mode 100644
index 0000000000..fe7cfb9030
--- /dev/null
+++ b/tests/manual/wasm/eventloop/asyncify_exec/CMakeLists.txt
@@ -0,0 +1,12 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+
+qt_internal_add_manual_test(asyncify_exec
+ SOURCES
+ main.cpp
+ LIBRARIES
+ Qt::Core
+)
+
+# Enable asyncify for this test. Also enable optimizations in order to reduce the binary size.
+target_link_options(asyncify_exec PUBLIC -sASYNCIFY -Os)
diff --git a/tests/manual/wasm/eventloop/asyncify_exec/main.cpp b/tests/manual/wasm/eventloop/asyncify_exec/main.cpp
new file mode 100644
index 0000000000..f09163184d
--- /dev/null
+++ b/tests/manual/wasm/eventloop/asyncify_exec/main.cpp
@@ -0,0 +1,25 @@
+// Copyright (C) 2021 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+#include <QtCore>
+
+// This test shows how to use asyncify to enable blocking the main
+// thread on QEventLoop::exec(), while event processing continues.
+int main(int argc, char **argv)
+{
+ QCoreApplication app(argc, argv);
+
+ QTimer::singleShot(1000, []() {
+
+ QEventLoop loop;
+ QTimer::singleShot(2000, [&loop]() {
+ qDebug() << "Calling QEventLoop::quit()";
+ loop.quit();
+ });
+
+ qDebug() << "Calling QEventLoop::exec()";
+ loop.exec();
+ qDebug() << "Returned from QEventLoop::exec()";
+ });
+
+ app.exec();
+}
diff --git a/tests/manual/wasm/eventloop/dialog_exec/CMakeLists.txt b/tests/manual/wasm/eventloop/dialog_exec/CMakeLists.txt
new file mode 100644
index 0000000000..ac18643c63
--- /dev/null
+++ b/tests/manual/wasm/eventloop/dialog_exec/CMakeLists.txt
@@ -0,0 +1,12 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+
+qt_internal_add_manual_test(dialog_exec
+ GUI
+ SOURCES
+ main.cpp
+ LIBRARIES
+ Qt::Core
+ Qt::Gui
+ Qt::Widgets
+)
diff --git a/tests/manual/wasm/eventloop/dialog_exec/main.cpp b/tests/manual/wasm/eventloop/dialog_exec/main.cpp
new file mode 100644
index 0000000000..f5b072fc0b
--- /dev/null
+++ b/tests/manual/wasm/eventloop/dialog_exec/main.cpp
@@ -0,0 +1,48 @@
+// Copyright (C) 2021 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+#include <QtGui>
+#include <QtWidgets>
+
+// This example show how calling QDialog::exec() shows the dialog,
+// but does not return.
+
+class ClickWindow: public QRasterWindow
+{
+public:
+ ClickWindow() {
+ qDebug() << "ClickWindow constructor";
+ }
+
+ ~ClickWindow() {
+ qDebug() << "ClickWindow destructor";
+ }
+
+ void paintEvent(QPaintEvent *ev) override {
+ QPainter p(this);
+ p.fillRect(ev->rect(), QColorConstants::Svg::deepskyblue);
+ p.drawText(50, 100, "Application has started. See the developer tools console for debug output");
+ }
+
+ void mousePressEvent(QMouseEvent *) override {
+ qDebug() << "mousePressEvent(): calling QMessageBox::exec()";
+
+ QMessageBox messageBox;
+ messageBox.setText("Hello! This is a message box.");
+ connect(&messageBox, &QMessageBox::buttonClicked, [](QAbstractButton *button) {
+ qDebug() << "Button Clicked" << button;
+ });
+ messageBox.exec(); // <-- does not return
+
+ qDebug() << "mousePressEvent(): done"; // <--- will not be printed
+ }
+};
+
+int main(int argc, char **argv)
+{
+ QApplication app(argc, argv);
+
+ ClickWindow window;
+ window.show();
+
+ return app.exec();
+}
diff --git a/tests/manual/wasm/eventloop/eventloop_auto/CMakeLists.txt b/tests/manual/wasm/eventloop/eventloop_auto/CMakeLists.txt
new file mode 100644
index 0000000000..9bfa875be7
--- /dev/null
+++ b/tests/manual/wasm/eventloop/eventloop_auto/CMakeLists.txt
@@ -0,0 +1,43 @@
+include_directories(../../qtwasmtestlib/)
+
+# default buid
+qt_internal_add_manual_test(eventloop_auto
+ SOURCES
+ main.cpp
+ ../../qtwasmtestlib/qtwasmtestlib.cpp
+ LIBRARIES
+ Qt::Core
+ Qt::CorePrivate
+)
+
+add_custom_command(
+ TARGET eventloop_auto POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E copy
+ ${CMAKE_CURRENT_SOURCE_DIR}/eventloop_auto.html
+ ${CMAKE_CURRENT_BINARY_DIR}/eventloop_auto.html)
+
+add_custom_command(
+ TARGET eventloop_auto POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E copy
+ ${CMAKE_CURRENT_SOURCE_DIR}/../../qtwasmtestlib/qtwasmtestlib.js
+ ${CMAKE_CURRENT_BINARY_DIR}/qtwasmtestlib.js)
+
+# asyncify enabled build
+qt_internal_add_manual_test(eventloop_auto_asyncify
+ SOURCES
+ main.cpp
+ ../../qtwasmtestlib/qtwasmtestlib.cpp
+ LIBRARIES
+ Qt::Core
+ Qt::CorePrivate
+)
+
+target_link_options(eventloop_auto_asyncify PRIVATE -sASYNCIFY -Os)
+
+add_custom_command(
+ TARGET eventloop_auto_asyncify POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E copy
+ ${CMAKE_CURRENT_SOURCE_DIR}/eventloop_auto_asyncify.html
+ ${CMAKE_CURRENT_BINARY_DIR}/eventloop_auto_asyncify.html)
+
+
diff --git a/tests/manual/wasm/eventloop/eventloop_auto/eventloop_auto.html b/tests/manual/wasm/eventloop/eventloop_auto/eventloop_auto.html
new file mode 100644
index 0000000000..e8e35abcbb
--- /dev/null
+++ b/tests/manual/wasm/eventloop/eventloop_auto/eventloop_auto.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<script type="text/javascript" src="qtwasmtestlib.js"></script>
+<script type="text/javascript" src="eventloop_auto.js"></script>
+<script>
+ window.onload = () => {
+ runTestCase(eventloop_auto_entry, document.getElementById("log"));
+ };
+</script>
+<p>Running event dispatcher auto test.</p>
+<div id="log"></div>
diff --git a/tests/manual/wasm/eventloop/eventloop_auto/eventloop_auto_asyncify.html b/tests/manual/wasm/eventloop/eventloop_auto/eventloop_auto_asyncify.html
new file mode 100644
index 0000000000..f09b29d85b
--- /dev/null
+++ b/tests/manual/wasm/eventloop/eventloop_auto/eventloop_auto_asyncify.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<script type="text/javascript" src="qtwasmtestlib.js"></script>
+<script type="text/javascript" src="eventloop_auto_asyncify.js"></script>
+<script>
+ window.onload = () => {
+ runTestCase(eventloop_auto_asyncify_entry, document.getElementById("log"));
+ };
+</script>
+<p>Running event dispatcher auto test.</p>
+<div id="log"></div>
diff --git a/tests/manual/wasm/eventloop/eventloop_auto/main.cpp b/tests/manual/wasm/eventloop/eventloop_auto/main.cpp
new file mode 100644
index 0000000000..32af372b62
--- /dev/null
+++ b/tests/manual/wasm/eventloop/eventloop_auto/main.cpp
@@ -0,0 +1,327 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#include <QtCore/QCoreApplication>
+#include <QtCore/QEvent>
+#include <QtCore/QMutex>
+#include <QtCore/QObject>
+#include <QtCore/QThread>
+#include <QtCore/QTimer>
+#include <QtCore/private/qstdweb_p.h>
+
+#include <qtwasmtestlib.h>
+
+#include "emscripten.h"
+
+const int timerTimeout = 10;
+
+class WasmEventDispatcherTest: public QObject
+{
+ Q_OBJECT
+private slots:
+ void postEventMainThread();
+ void timerMainThread();
+ void timerMainThreadMultiple();
+
+#if QT_CONFIG(thread)
+ void postEventSecondaryThread();
+ void postEventSecondaryThreads();
+ void postEventToSecondaryThread();
+ void timerSecondaryThread();
+#endif
+
+ void postEventAsyncify();
+ void timerAsyncify();
+ void postEventAsyncifyLoop();
+
+private:
+// Disabled test function: Asyncify wait on pthread_join is not supported,
+// see https://github.com/emscripten-core/emscripten/issues/9910
+#if QT_CONFIG(thread)
+ void threadAsyncifyWait();
+#endif
+};
+
+class EventTarget : public QObject
+{
+ Q_OBJECT
+
+public:
+ static EventTarget *create(std::function<void()> callback)
+ {
+ return new EventTarget(callback);
+ }
+
+ static QEvent *createEvent()
+ {
+ return new QEvent(QEvent::User);
+ }
+
+protected:
+ EventTarget(std::function<void()> callback)
+ : m_callback(callback) { }
+
+ bool event(QEvent *evt)
+ {
+ if (evt->type() == QEvent::User) {
+ m_callback();
+ deleteLater();
+ return true;
+ }
+ return QObject::event(evt);
+ }
+
+private:
+ std::function<void()> m_callback;
+};
+
+class CompleteTestFunctionRefGuard {
+public:
+ CompleteTestFunctionRefGuard(CompleteTestFunctionRefGuard const&) = delete;
+ CompleteTestFunctionRefGuard& operator=(CompleteTestFunctionRefGuard const&) = delete;
+
+ static CompleteTestFunctionRefGuard *create() {
+ return new CompleteTestFunctionRefGuard();
+ }
+
+ void ref() {
+ QMutexLocker lock(&mutex);
+ ++m_counter;
+ }
+
+ void deref() {
+ const bool finalDeref = [this] {
+ QMutexLocker lock(&mutex);
+ return --m_counter == 0;
+ }();
+
+ if (finalDeref)
+ QtWasmTest::completeTestFunction();
+ }
+private:
+ CompleteTestFunctionRefGuard() { };
+
+ QMutex mutex;
+ int m_counter = 0;
+};
+
+#if QT_CONFIG(thread)
+
+class TestThread : public QThread
+{
+public:
+ static QThread *create(std::function<void()> started, std::function<void()> finished)
+ {
+ TestThread *thread = new TestThread();
+ connect(thread, &QThread::started, [started]() {
+ started();
+ });
+ connect(thread, &QThread::finished, [thread, finished]() {
+ finished();
+ thread->deleteLater();
+ });
+ thread->start();
+ return thread;
+ }
+};
+
+#endif
+
+// Post event to the main thread and verify that it is processed.
+void WasmEventDispatcherTest::postEventMainThread()
+{
+ QCoreApplication::postEvent(EventTarget::create([](){
+ QtWasmTest::completeTestFunction();
+ }), EventTarget::createEvent());
+}
+
+// Create a timer on the main thread and verify that it fires
+void WasmEventDispatcherTest::timerMainThread()
+{
+ QTimer::singleShot(timerTimeout, [](){
+ QtWasmTest::completeTestFunction();
+ });
+}
+
+void WasmEventDispatcherTest::timerMainThreadMultiple()
+{
+ CompleteTestFunctionRefGuard *completeGuard = CompleteTestFunctionRefGuard::create();
+ int timers = 10;
+ for (int i = 0; i < timers; ++i) {
+ completeGuard->ref();
+ QTimer::singleShot(timerTimeout * i, [completeGuard](){
+ completeGuard->deref();
+ });
+ }
+}
+
+#if QT_CONFIG(thread)
+
+// Post event on a secondary thread and verify that it is processed.
+void WasmEventDispatcherTest::postEventSecondaryThread()
+{
+ auto started = [](){
+ QCoreApplication::postEvent(EventTarget::create([](){
+ QThread::currentThread()->quit();
+ }), EventTarget::createEvent());
+ };
+
+ auto finished = [](){
+ QtWasmTest::completeTestFunction();
+ };
+
+ TestThread::create(started, finished);
+}
+
+// Post event _to_ a secondary thread and verify that it is processed.
+void WasmEventDispatcherTest::postEventToSecondaryThread()
+{
+ auto started = [](){};
+ auto finished = [](){
+ QtWasmTest::completeTestFunction();
+ };
+
+ QThread *t = TestThread::create(started, finished);
+ EventTarget *target = EventTarget::create([](){
+ QThread::currentThread()->quit();
+ });
+ target->moveToThread(t);
+ QCoreApplication::postEvent(target, EventTarget::createEvent());
+}
+
+// Post events to many secondary threads and verify that they are processed.
+void WasmEventDispatcherTest::postEventSecondaryThreads()
+{
+ // This test completes afer all threads has finished
+ CompleteTestFunctionRefGuard *completeGuard = CompleteTestFunctionRefGuard::create();
+ completeGuard->ref(); // including this thread
+
+ auto started = [](){
+ QCoreApplication::postEvent(EventTarget::create([](){
+ QThread::currentThread()->quit();
+ }), EventTarget::createEvent());
+ };
+
+ auto finished = [completeGuard](){
+ completeGuard->deref();
+ };
+
+ // Start a nymber of threads in parallel, keeping in mind that the browser
+ // has some max number of concurrent web workers (maybe 20), and that starting
+ // a new web worker requires completing a GET network request for the worker's JS.
+ const int numThreads = 10;
+ for (int i = 0; i < numThreads; ++i) {
+ completeGuard->ref();
+ TestThread::create(started, finished);
+ }
+
+ completeGuard->deref();
+}
+
+// Create a timer a secondary thread and verify that it fires
+void WasmEventDispatcherTest::timerSecondaryThread()
+{
+ auto started = [](){
+ QTimer::singleShot(timerTimeout, [](){
+ QThread::currentThread()->quit();
+ });
+ };
+
+ auto finished = [](){
+ QtWasmTest::completeTestFunction();
+ };
+
+ TestThread::create(started, finished);
+}
+
+#endif
+
+// Post an event to the main thread and asyncify wait for it
+void WasmEventDispatcherTest::postEventAsyncify()
+{
+ if (!qstdweb::haveAsyncify()) {
+ QtWasmTest::completeTestFunction(QtWasmTest::TestResult::Skip, "requires asyncify");
+ return;
+ }
+
+ QEventLoop loop;
+ QCoreApplication::postEvent(EventTarget::create([&loop](){
+ loop.quit();
+ }), EventTarget::createEvent());
+ loop.exec();
+
+ QtWasmTest::completeTestFunction();
+}
+
+// Create a timer on the main thread and asyncify wait for it
+void WasmEventDispatcherTest::timerAsyncify()
+{
+ if (!qstdweb::haveAsyncify()) {
+ QtWasmTest::completeTestFunction(QtWasmTest::TestResult::Skip, "requires asyncify");
+ return;
+ }
+
+ QEventLoop loop;
+ QTimer::singleShot(timerTimeout, [&loop](){
+ loop.quit();
+ });
+ loop.exec();
+
+ QtWasmTest::completeTestFunction();
+}
+
+// Asyncify wait in a loop
+void WasmEventDispatcherTest::postEventAsyncifyLoop()
+{
+ if (!qstdweb::haveAsyncify()) {
+ QtWasmTest::completeTestFunction(QtWasmTest::TestResult::Skip, "requires asyncify");
+ return;
+ }
+
+ for (int i = 0; i < 10; ++i) {
+ QEventLoop loop;
+ QCoreApplication::postEvent(EventTarget::create([&loop]() {
+ loop.quit();
+ }), EventTarget::createEvent());
+ loop.exec();
+ }
+
+ QtWasmTest::completeTestFunction();
+}
+
+#if QT_CONFIG(thread)
+// Asyncify wait for QThread::wait() / pthread_join()
+void WasmEventDispatcherTest::threadAsyncifyWait()
+{
+ if (!qstdweb::haveAsyncify())
+ QtWasmTest::completeTestFunction(QtWasmTest::TestResult::Skip, "requires asyncify");
+
+ const int threadCount = 15;
+
+ QVector<QThread *> threads;
+ threads.reserve(threadCount);
+
+ for (int i = 0; i < threadCount; ++i) {
+ QThread *thread = new QThread();
+ threads.push_back(thread);
+ thread->start();
+ }
+
+ for (int i = 0; i < threadCount; ++i) {
+ QThread *thread = threads[i];
+ thread->wait();
+ delete thread;
+ }
+
+ QtWasmTest::completeTestFunction();
+}
+#endif
+
+int main(int argc, char **argv)
+{
+ auto testObject = std::make_shared<WasmEventDispatcherTest>();
+ QtWasmTest::initTestCase<QCoreApplication>(argc, argv, testObject);
+ return 0;
+}
+
+#include "main.moc"
diff --git a/tests/manual/wasm/eventloop/main_exec/CMakeLists.txt b/tests/manual/wasm/eventloop/main_exec/CMakeLists.txt
new file mode 100644
index 0000000000..1f263ddbcf
--- /dev/null
+++ b/tests/manual/wasm/eventloop/main_exec/CMakeLists.txt
@@ -0,0 +1,11 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+
+qt_internal_add_manual_test(main_exec
+ GUI
+ SOURCES
+ main.cpp
+ LIBRARIES
+ Qt::Core
+ Qt::Gui
+)
diff --git a/tests/manual/wasm/eventloop/main_exec/main.cpp b/tests/manual/wasm/eventloop/main_exec/main.cpp
new file mode 100644
index 0000000000..17eccafe18
--- /dev/null
+++ b/tests/manual/wasm/eventloop/main_exec/main.cpp
@@ -0,0 +1,67 @@
+// Copyright (C) 2021 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+#include <QtGui>
+
+// This example demonstrates how the standard Qt main()
+// pattern works on Emscripten/WebAssambly, where exec()
+// does not return.
+
+class ClickWindow: public QRasterWindow
+{
+public:
+ ClickWindow() {
+ qDebug() << "ClickWindow constructor";
+ }
+ ~ClickWindow() {
+ qDebug() << "ClickWindow destructor";
+ }
+
+ void paintEvent(QPaintEvent *ev) override {
+ QPainter p(this);
+ p.fillRect(ev->rect(), QColorConstants::Svg::deepskyblue);
+ p.drawText(50, 100, "Application has started. See the developer tools console for debug output");
+ }
+
+ void mousePressEvent(QMouseEvent *) override {
+ qDebug() << "mousePressEvent(): calling QGuiApplication::quit()";
+ QGuiApplication::quit();
+ }
+};
+
+int main(int argc, char **argv)
+{
+ qDebug() << "main(): Creating QGuiApplication object";
+ QGuiApplication app(argc, argv);
+
+ QObject::connect(&app, &QCoreApplication::aboutToQuit, [](){
+ qDebug() << "QCoreApplication::aboutToQuit";
+ });
+
+ ClickWindow window;
+ window.show();
+
+ qDebug() << "main(): calling exec()";
+ app.exec();
+
+ // The exec() call above never returns; instead, a JavaScript exception
+ // is thrown such that control returns to the browser while preserving
+ // the C++ stack.
+
+ // This means that the window object above is not destroyed, and that
+ // shutdown code after exec() does not run.
+
+ qDebug() << "main(): after exit"; // <- will not be printed
+}
+
+// Global variables are created before main() as usual, but not destroyed
+class Global
+{
+public:
+ Global() {
+ qDebug() << "Global constructor";
+ }
+ ~Global() {
+ qDebug() << "Global destructor"; // <- will not be printed
+ }
+};
+Global global;
diff --git a/tests/manual/wasm/eventloop/main_noexec/CMakeLists.txt b/tests/manual/wasm/eventloop/main_noexec/CMakeLists.txt
new file mode 100644
index 0000000000..e929089479
--- /dev/null
+++ b/tests/manual/wasm/eventloop/main_noexec/CMakeLists.txt
@@ -0,0 +1,11 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+
+qt_internal_add_manual_test(main_noexec
+ GUI
+ SOURCES
+ main.cpp
+ LIBRARIES
+ Qt::Core
+ Qt::Gui
+)
diff --git a/tests/manual/wasm/eventloop/main_noexec/main.cpp b/tests/manual/wasm/eventloop/main_noexec/main.cpp
new file mode 100644
index 0000000000..6ddd88bd14
--- /dev/null
+++ b/tests/manual/wasm/eventloop/main_noexec/main.cpp
@@ -0,0 +1,66 @@
+// Copyright (C) 2021 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+#include <QtGui>
+
+// This example demonstrates how to create QGuiApplication
+// without calling exec(), and then exiting main() without
+// shutting down the Qt event loop.
+
+class ClickWindow: public QRasterWindow
+{
+public:
+
+ ClickWindow() {
+ qDebug() << "ClickWindow constructor";
+ }
+ ~ClickWindow() {
+ qDebug() << "ClickWindow destructor";
+ }
+
+ void paintEvent(QPaintEvent *ev) override {
+ QPainter p(this);
+ p.fillRect(ev->rect(), QColorConstants::Svg::deepskyblue);
+ p.drawText(50, 100, "Application has started. See the developer tools console for debug output");
+ }
+
+ void mousePressEvent(QMouseEvent *) override {
+ qDebug() << "mousePressEvent(): calling QGuiApplication::quit()";
+ QGuiApplication::quit();
+ }
+};
+
+int main(int argc, char **argv)
+{
+ qDebug() << "main(): Creating QGuiApplication object";
+ QGuiApplication *app = new QGuiApplication(argc, argv);
+
+ QObject::connect(app, &QCoreApplication::aboutToQuit, [](){
+ qDebug() << "QCoreApplication::aboutToQuit";
+ });
+
+ qDebug() << "main(): Creating ClickWindow object";
+ ClickWindow *window = new ClickWindow();
+ window->show();
+
+ // We can exit main; the Qt event loop and the emscripten runtime
+ // will keep running, as long as Emscriptens EXIT_RUNTIME option
+ // has not been enabled.
+
+ qDebug() << "main(): exit";
+}
+
+// Global variables are created before main() as usual, but not destroyed
+class Global
+{
+public:
+ Global() {
+ qDebug() << "Global constructor";
+ }
+ ~Global() {
+ qDebug() << "Global destructor"; // <- will not be printed
+ }
+};
+Global global;
+
+
+
diff --git a/tests/manual/wasm/eventloop/thread_exec/CMakeLists.txt b/tests/manual/wasm/eventloop/thread_exec/CMakeLists.txt
new file mode 100644
index 0000000000..765ccee4f1
--- /dev/null
+++ b/tests/manual/wasm/eventloop/thread_exec/CMakeLists.txt
@@ -0,0 +1,11 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+
+qt_internal_add_manual_test(thread_exec
+ GUI
+ SOURCES
+ main.cpp
+ LIBRARIES
+ Qt::Core
+ Qt::Gui
+)
diff --git a/tests/manual/wasm/eventloop/thread_exec/main.cpp b/tests/manual/wasm/eventloop/thread_exec/main.cpp
new file mode 100644
index 0000000000..589066b34d
--- /dev/null
+++ b/tests/manual/wasm/eventloop/thread_exec/main.cpp
@@ -0,0 +1,75 @@
+// Copyright (C) 2021 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+#include <QtGui>
+
+class EventTarget : public QObject
+{
+ Q_OBJECT
+protected:
+ bool event(QEvent *evt)
+ {
+ if (evt->type() == QEvent::User) {
+ qDebug() << "User event on thread" << QThread::currentThread();
+ return true;
+ }
+ return QObject::event(evt);
+ }
+};
+
+class EventPosterWindow: public QRasterWindow
+{
+public:
+ EventPosterWindow(EventTarget *target)
+ :m_target(target)
+ { }
+
+ void paintEvent(QPaintEvent *ev) override {
+ QPainter p(this);
+ p.fillRect(ev->rect(), QColorConstants::Svg::deepskyblue);
+ p.drawText(50, 100, "Application has started. Click to post events.\n See the developer tools console for debug output");
+ }
+
+ void mousePressEvent(QMouseEvent *) override {
+ qDebug() << "Posting events from thread" << QThread::currentThread();
+ QGuiApplication::postEvent(m_target, new QEvent(QEvent::User));
+ QTimer::singleShot(500, m_target, []() {
+ qDebug() << "Timer event on secondary thread" << QThread::currentThread();
+ });
+ }
+
+public:
+ EventTarget *m_target;
+};
+
+class SecondaryThread : public QThread
+{
+public:
+ void run() override {
+ qDebug() << "exec on secondary thread" << QThread::currentThread();
+ exec();
+ }
+};
+
+// This example demonstrates how to start a secondary thread event loop
+int main(int argc, char **argv)
+{
+ QGuiApplication app(argc, argv);
+
+ EventTarget eventTarget;
+
+ EventPosterWindow window(&eventTarget);
+ window.show();
+
+ SecondaryThread thread;
+ eventTarget.moveToThread(&thread);
+
+#if QT_CONFIG(thread)
+ thread.start();
+#else
+ qDebug() << "Warning: This test requires a multithreaded build of Qt for WebAssembly";
+#endif
+
+ return app.exec();
+}
+
+#include "main.moc"
diff --git a/tests/manual/wasm/eventloop/thread_exec/thread_exec.html b/tests/manual/wasm/eventloop/thread_exec/thread_exec.html
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/manual/wasm/eventloop/thread_exec/thread_exec.html
diff --git a/tests/manual/wasm/localfiles/CMakeLists.txt b/tests/manual/wasm/localfiles/CMakeLists.txt
new file mode 100644
index 0000000000..6d607d1f5a
--- /dev/null
+++ b/tests/manual/wasm/localfiles/CMakeLists.txt
@@ -0,0 +1,12 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+
+qt_internal_add_manual_test(localfiles
+ GUI
+ SOURCES
+ main.cpp
+ LIBRARIES
+ Qt::Core
+ Qt::Gui
+ Qt::Widgets
+)
diff --git a/tests/manual/wasm/localfiles/main.cpp b/tests/manual/wasm/localfiles/main.cpp
index 9dfc30885c..862bff50a4 100644
--- a/tests/manual/wasm/localfiles/main.cpp
+++ b/tests/manual/wasm/localfiles/main.cpp
@@ -1,100 +1,138 @@
-/****************************************************************************
-**
-** Copyright (C) 2019 The Qt Company Ltd.
-** Contact: https://www.qt.io/licensing/
-**
-** This file is part of the examples of the Qt Toolkit.
-**
-** $QT_BEGIN_LICENSE:BSD$
-** Commercial License Usage
-** Licensees holding valid commercial Qt licenses may use this file in
-** accordance with the commercial license agreement provided with the
-** Software or, alternatively, in accordance with the terms contained in
-** a written agreement between you and The Qt Company. For licensing terms
-** and conditions see https://www.qt.io/terms-conditions. For further
-** information use the contact form at https://www.qt.io/contact-us.
-**
-** BSD License Usage
-** Alternatively, you may use this file under the terms of the BSD license
-** as follows:
-**
-** "Redistribution and use in source and binary forms, with or without
-** modification, are permitted provided that the following conditions are
-** met:
-** * Redistributions of source code must retain the above copyright
-** notice, this list of conditions and the following disclaimer.
-** * Redistributions in binary form must reproduce the above copyright
-** notice, this list of conditions and the following disclaimer in
-** the documentation and/or other materials provided with the
-** distribution.
-** * Neither the name of The Qt Company Ltd nor the names of its
-** contributors may be used to endorse or promote products derived
-** from this software without specific prior written permission.
-**
-**
-** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
-**
-** $QT_END_LICENSE$
-**
-****************************************************************************/
+// Copyright (C) 2019 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <QtWidgets/QtWidgets>
+#include <emscripten/val.h>
+#include <emscripten.h>
+
+class AppWindow : public QObject
+{
+Q_OBJECT
+public:
+ AppWindow() : m_layout(new QVBoxLayout(&m_loadFileUi)),
+ m_window(emscripten::val::global("window")),
+ m_showOpenFilePickerFunction(m_window["showOpenFilePicker"]),
+ m_showSaveFilePickerFunction(m_window["showSaveFilePicker"])
+ {
+ addWidget<QLabel>("Filename filter");
+
+ const bool localFileApiAvailable =
+ !m_showOpenFilePickerFunction.isUndefined() && !m_showSaveFilePickerFunction.isUndefined();
+
+ m_useLocalFileApisCheckbox = addWidget<QCheckBox>("Use the window.showXFilePicker APIs");
+ m_useLocalFileApisCheckbox->setEnabled(localFileApiAvailable);
+ m_useLocalFileApisCheckbox->setChecked(localFileApiAvailable);
+
+ m_filterEdit = addWidget<QLineEdit>("Images (*.png *.jpg);;PDF (*.pdf);;*.txt");
+
+ auto* loadFile = addWidget<QPushButton>("Load File");
+
+ m_fileInfo = addWidget<QLabel>("Opened file:");
+ m_fileInfo->setTextInteractionFlags(Qt::TextSelectableByMouse);
+
+ m_fileHash = addWidget<QLabel>("Sha256:");
+ m_fileHash->setTextInteractionFlags(Qt::TextSelectableByMouse);
+
+ addWidget<QLabel>("Saved file name");
+ m_savedFileNameEdit = addWidget<QLineEdit>("qttestresult");
+
+ m_saveFile = addWidget<QPushButton>("Save File");
+ m_saveFile->setEnabled(false);
+
+ m_layout->addStretch();
+
+ m_loadFileUi.setLayout(m_layout);
+
+ QObject::connect(m_useLocalFileApisCheckbox, &QCheckBox::toggled, std::bind(&AppWindow::onUseLocalFileApisCheckboxToggled, this));
+
+ QObject::connect(loadFile, &QPushButton::clicked, this, &AppWindow::onLoadClicked);
+
+ QObject::connect(m_saveFile, &QPushButton::clicked, std::bind(&AppWindow::onSaveClicked, this));
+ }
+
+ void show() {
+ m_loadFileUi.show();
+ }
+
+ ~AppWindow() = default;
+
+private Q_SLOTS:
+ void onUseLocalFileApisCheckboxToggled()
+ {
+ m_window.set("showOpenFilePicker",
+ m_useLocalFileApisCheckbox->isChecked() ?
+ m_showOpenFilePickerFunction : emscripten::val::undefined());
+ m_window.set("showSaveFilePicker",
+ m_useLocalFileApisCheckbox->isChecked() ?
+ m_showSaveFilePickerFunction : emscripten::val::undefined());
+ }
+
+ void onFileContentReady(const QString &fileName, const QByteArray &fileContents)
+ {
+ m_fileContent = fileContents;
+ m_fileInfo->setText(QString("Opened file: %1 size: %2").arg(fileName).arg(fileContents.size()));
+ m_saveFile->setEnabled(true);
+
+ QTimer::singleShot(100, this, &AppWindow::computeAndDisplayFileHash); // update UI before computing hash
+ }
+
+ void computeAndDisplayFileHash()
+ {
+ QByteArray hash = QCryptographicHash::hash(m_fileContent, QCryptographicHash::Sha256);
+ m_fileHash->setText(QString("Sha256: %1").arg(QString(hash.toHex())));
+ }
+
+ void onFileSaved(bool success)
+ {
+ m_fileInfo->setText(QString("File save result: %1").arg(success ? "success" : "failed"));
+ }
+
+ void onLoadClicked()
+ {
+ QFileDialog::getOpenFileContent(
+ m_filterEdit->text(),
+ std::bind(&AppWindow::onFileContentReady, this, std::placeholders::_1, std::placeholders::_2),
+ &m_loadFileUi);
+ }
+
+ void onSaveClicked()
+ {
+ m_fileInfo->setText("Saving file... (no result information with current API)");
+ QFileDialog::saveFileContent(m_fileContent, m_savedFileNameEdit->text());
+ }
+
+private:
+ template <class T, class... Args>
+ T* addWidget(Args... args)
+ {
+ T* widget = new T(std::forward<Args>(args)..., &m_loadFileUi);
+ m_layout->addWidget(widget);
+ return widget;
+ }
+
+ QWidget m_loadFileUi;
+
+ QCheckBox* m_useLocalFileApisCheckbox;
+ QLineEdit* m_filterEdit;
+ QVBoxLayout *m_layout;
+ QLabel* m_fileInfo;
+ QLabel* m_fileHash;
+ QLineEdit* m_savedFileNameEdit;
+ QPushButton* m_saveFile;
+
+ emscripten::val m_window;
+ emscripten::val m_showOpenFilePickerFunction;
+ emscripten::val m_showSaveFilePickerFunction;
+
+ QByteArray m_fileContent;
+};
+
int main(int argc, char **argv)
{
- QApplication app(argc, argv);
-
- QByteArray content;
-
- QWidget loadFileUi;
- QVBoxLayout *layout = new QVBoxLayout();
- QPushButton *loadFile = new QPushButton("Load File");
- QLabel *fileInfo = new QLabel("Opened file:");
- fileInfo->setTextInteractionFlags(Qt::TextSelectableByMouse);
- QLabel *fileHash = new QLabel("Sha256:");
- fileHash->setTextInteractionFlags(Qt::TextSelectableByMouse);
- QPushButton *saveFile = new QPushButton("Save File");
- saveFile->setEnabled(false);
-
- auto onFileReady = [=, &content](const QString &fileName, const QByteArray &fileContents) {
- content = fileContents;
- fileInfo->setText(QString("Opened file: %1 size: %2").arg(fileName).arg(fileContents.size()));
- saveFile->setEnabled(true);
-
- auto computeDisplayFileHash = [=](){
- QByteArray hash = QCryptographicHash::hash(fileContents, QCryptographicHash::Sha256);
- fileHash->setText(QString("Sha256: %1").arg(QString(hash.toHex())));
- };
-
- QTimer::singleShot(100, computeDisplayFileHash); // update UI before computing hash
- };
- auto onLoadClicked = [=](){
- QFileDialog::getOpenFileContent("*.*", onFileReady);
- };
- QObject::connect(loadFile, &QPushButton::clicked, onLoadClicked);
-
- auto onSaveClicked = [=, &content]() {
- QFileDialog::saveFileContent(content, "qtsavefiletest.dat");
- };
- QObject::connect(saveFile, &QPushButton::clicked, onSaveClicked);
-
- layout->addWidget(loadFile);
- layout->addWidget(fileInfo);
- layout->addWidget(fileHash);
- layout->addWidget(saveFile);
- layout->addStretch();
-
- loadFileUi.setLayout(layout);
- loadFileUi.show();
-
- return app.exec();
+ QApplication application(argc, argv);
+ AppWindow window;
+ window.show();
+ return application.exec();
}
+
+#include "main.moc"
diff --git a/tests/manual/wasm/localfonts/CMakeLists.txt b/tests/manual/wasm/localfonts/CMakeLists.txt
new file mode 100644
index 0000000000..b5df4ad9fa
--- /dev/null
+++ b/tests/manual/wasm/localfonts/CMakeLists.txt
@@ -0,0 +1,4 @@
+# Copyright (C) 2023 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+
+add_subdirectory(fontloading)
diff --git a/tests/manual/wasm/localfonts/fontloading/CMakeLists.txt b/tests/manual/wasm/localfonts/fontloading/CMakeLists.txt
new file mode 100644
index 0000000000..c3dc37d27d
--- /dev/null
+++ b/tests/manual/wasm/localfonts/fontloading/CMakeLists.txt
@@ -0,0 +1,20 @@
+# Copyright (C) 2023 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+
+qt_internal_add_manual_test(fontloading
+ GUI
+ SOURCES
+ main.cpp
+ LIBRARIES
+ Qt::Core
+ Qt::Gui
+ Qt::Widgets
+)
+
+add_custom_command(
+ TARGET fontloading POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E copy
+ ${CMAKE_CURRENT_SOURCE_DIR}/fontloading.html
+ ${CMAKE_CURRENT_BINARY_DIR}/fontloading.html)
+#add_custom_target(html DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/fontloading.html)
+#add_dependencies(fontloading html)
diff --git a/tests/manual/wasm/localfonts/fontloading/fontloading.html b/tests/manual/wasm/localfonts/fontloading/fontloading.html
new file mode 100644
index 0000000000..619217205b
--- /dev/null
+++ b/tests/manual/wasm/localfonts/fontloading/fontloading.html
@@ -0,0 +1,167 @@
+<!doctype html>
+
+<script src="qtloader.js"></script>
+<script src="fontloading.js"></script>
+
+<style>
+ body {
+ padding: 5px;
+ }
+
+ .container {
+ display: flex;
+ }
+
+ .column {
+ flex: 1;
+ }
+</style>
+
+
+<script>
+
+ // UI
+ let familyCount;
+ let families;
+
+ // Data
+ let fontFamilies = new Set();
+ let loadStartTime;
+
+ // App
+ let instance;
+
+ async function updatePermissionStatus() {
+ let permissonStatusElement = document.getElementById("permissonStatus");
+ let permissionStatus = await navigator.permissions.query({ name: "local-fonts" })
+ permissonStatusElement.innerHTML = permissionStatus.state;
+ }
+
+ window.onload = async () => {
+ let supported = document.getElementById("supported");
+ let permissonStatus = document.getElementById("permissonStatus");
+ let permission = document.getElementById("permission");
+ let defaultfonts = document.getElementById("defaultfonts");
+ let allfonts = document.getElementById("allfonts");
+ let start = document.getElementById("start");
+ let loadFonts = document.getElementById("loadFonts");
+ startupTime = document.getElementById("startupTime");
+ familyCount = document.getElementById("familyCount");
+ families = document.getElementById("families");
+
+ fontFamilies.clear();
+
+ let localFontsAccessSupported = window.queryLocalFonts ? true : false
+ if (localFontsAccessSupported) {
+ supported.innerHTML = "True"
+ } else {
+ supported.innerHTML = "False"
+ return;
+ }
+
+ updatePermissionStatus();
+
+ const module = WebAssembly.compileStreaming(fetch('fontloading.wasm'));
+
+ start.onclick = async () => {
+
+ // Delete any previous instance.
+ if (instance) {
+ instance.deleteapp(); // see main.cpp
+ instance = null;
+ }
+
+ loadStartTime = performance.now();
+ startupTime.innerHTML = 0;
+ familyCount.innerHTML = 0;
+ let localFontFamilyLoadCollection = "NoFontFamilies"
+ if (defaultfonts.checked)
+ localFontFamilyLoadCollection = "DefaultFontFamilies"
+ else if (allfonts.checked)
+ localFontFamilyLoadCollection = "AllFontFamilies"
+
+ let qtcontainer = document.getElementById("qtcontainer");
+ qtcontainer.innerHTML = ""; // clear children
+ qtcontainer.style.visibility = "hidden";
+
+ let extraFonts = document.getElementById("extrafonts").value.split(",");
+
+ let config = {
+ qt: {
+ module: module,
+ containerElements: [qtcontainer],
+ onLoaded: () => {
+ console.log("JS: onLoaded")
+ qtcontainer.style.visibility = "visible";
+ },
+ entryFunction: window.fontloading_entry,
+ localFonts: {
+ requestPermission: permission.checked,
+ familiesCollection: localFontFamilyLoadCollection,
+ extraFamilies: extraFonts,
+ }
+ }
+ }
+ instance = await qtLoad(config);
+
+ updatePermissionStatus();
+ }
+
+ loadFonts.onclick = async () => {
+ loadStartTime = null; // disable timer
+ let fontsFamilies = document.getElementById("extraRuntimeFontFamilies").value.split(",");
+ console.log("extraRuntimeFontFamilies: " + fontsFamilies);
+ instance.qtLoadLocalFontFamilies(fontsFamilies);
+ }
+ };
+
+ function fontFamiliesLoaded(count) {
+ familyCount.innerHTML = count;
+ if (loadStartTime) {
+ elapsed = performance.now() - loadStartTime;
+ startupTime.innerHTML = Math.round(elapsed + 1);
+ }
+ }
+
+ function fontFamilyLoaded(family) {
+ fontFamilies.add(family);
+ }
+
+</script>
+
+<h2>Local Font Loading Test</h2>
+<p>Click "Load" button below to load the Qt test app with the specified settings. This test provides additional logs on the JavaScript console.</p>
+
+<div class="container">
+ <div class="column">
+ <span>Browser supports the Local Font Access API: </span><span id="supported" style="font-weight: bold;"></span><br>
+ <span>Local Font Access permission status: </span><span id="permissonStatus" style="font-weight: bold;"></span><br>
+ <br>
+ <input type="checkbox" id="permission"><label for="permission">Ask for Local Font access permission on startup</label><br>
+ <input type="radio" id="nofonts" name="fontset"></input><label for="nofonts">No local fonts</label><br>
+ <input type="radio" id="defaultfonts" name="fontset" checked></input><label for="defaultfonts">Default local fonts (web-safe fonts)</label><br>
+ <input type="radio" id="allfonts" name="fontset"></input><label for="allfonts">All local fonts (warning: extremely slow)</label><br>
+ <br>
+ <label for="extrafonts">Extra Font Families (comma separated) </label><input type="text" id="extrafonts" value=""></input><br>
+ <br>
+ <input type="checkbox" id="permission"><label for="permission">Enable 'Fonts' Logging Category</label><br>
+ <input type="checkbox" id="permission"><label for="permission">Enable Font Streaming</label><br>
+ <br>
+ <button type="button" id="start">Start Application</button><br>
+ <br>
+
+ <span>Startup time: </span><span id="startupTime"></span><br>
+ <span>Font family count: </span><span id="familyCount"></span><br>
+ <span>Font families: </span><span id="families"></span><br>
+ <br>
+
+ <button type="button" id="loadFonts">Load Extra Fonts</button>
+ <input type="text" id="extraRuntimeFontFamilies" value=""></input><br>
+ </div>
+
+ <div class="column">
+ <div id="qtcontainer" style="width: 100%; height: 300px; visibility: hidden;"></div>
+ </div>
+</div>
+
+
diff --git a/tests/manual/wasm/localfonts/fontloading/main.cpp b/tests/manual/wasm/localfonts/fontloading/main.cpp
new file mode 100644
index 0000000000..3824c8b871
--- /dev/null
+++ b/tests/manual/wasm/localfonts/fontloading/main.cpp
@@ -0,0 +1,78 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#include <QtGui>
+#include <QtWidgets>
+
+#include <emscripten/bind.h>
+#include <emscripten/val.h>
+
+using namespace emscripten;
+
+class FontViewer : public QWidget
+{
+public:
+ FontViewer() {
+ QTextEdit *edit = new QTextEdit;
+ edit->setPlainText("The quick brown fox jumps over the lazy dog\nHow quickly daft jumping zebras vex\nPack my box with five dozen liquor jugs");
+
+ QComboBox *combo = new QComboBox;
+ combo->addItems(QFontDatabase::families());
+
+ connect(combo, &QComboBox::currentTextChanged, [=](const QString &family) {
+ QFont font(family);
+ edit->setFont(font);
+ });
+
+ QObject::connect(qApp, &QGuiApplication::fontDatabaseChanged, [=]() {
+ QStringList families = QFontDatabase::families();
+ combo->clear();
+ combo->addItems(families);
+ });
+
+ QLayout *layout = new QVBoxLayout;
+ layout->addWidget(edit);
+ layout->addWidget(combo);
+ setLayout(layout);
+ }
+};
+
+FontViewer *g_viewer = nullptr;
+QApplication *g_app = nullptr;
+
+void deleteapp() {
+ delete g_viewer;
+ delete g_app;
+};
+
+EMSCRIPTEN_BINDINGS(fonloading) {
+ function("deleteapp", &deleteapp);
+}
+
+int main(int argc, char **argv)
+{
+ qDebug() << "C++ main: Creating application";
+ g_app = new QApplication(argc, argv);
+
+ // Make sure there is one call to fontFamiliesLoaded at startup,
+ // even if no further fonts are loaded.
+ QTimer::singleShot(0, [=]() {
+ emscripten::val window = emscripten::val::global("window");
+ window.call<void>("fontFamiliesLoaded", QFontDatabase::families().count());
+ });
+
+ g_viewer = new FontViewer();
+ g_viewer->show();
+
+ QObject::connect(g_app, &QGuiApplication::fontDatabaseChanged, [=]() {
+ QStringList families = QFontDatabase::families();
+
+ emscripten::val window = emscripten::val::global("window");
+
+ window.call<void>("fontFamiliesLoaded", families.count());
+ for (int i = 0; i < families.count(); ++i) {
+ window.call<void>("fontFamilyLoaded", families[i].toStdString());
+ }
+ });
+}
+
diff --git a/tests/manual/wasm/network/CMakeLists.txt b/tests/manual/wasm/network/CMakeLists.txt
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/manual/wasm/network/CMakeLists.txt
diff --git a/tests/manual/wasm/network/echo_client_mainthread/CMakeLists.txt b/tests/manual/wasm/network/echo_client_mainthread/CMakeLists.txt
new file mode 100644
index 0000000000..05416c0b66
--- /dev/null
+++ b/tests/manual/wasm/network/echo_client_mainthread/CMakeLists.txt
@@ -0,0 +1,8 @@
+qt_internal_add_manual_test(echo_client_mainthread
+ GUI
+ SOURCES
+ main.cpp
+ LIBRARIES
+ Qt::Core
+ Qt::Network
+)
diff --git a/tests/manual/wasm/network/echo_client_mainthread/main.cpp b/tests/manual/wasm/network/echo_client_mainthread/main.cpp
new file mode 100644
index 0000000000..ef696e5978
--- /dev/null
+++ b/tests/manual/wasm/network/echo_client_mainthread/main.cpp
@@ -0,0 +1,52 @@
+// Copyright (C) 2021 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#include <QtCore>
+#include <QtNetwork>
+
+int main(int argc, char **argv) {
+
+ QCoreApplication app(argc, argv);
+
+ // This example connects to localhost, but note that the host can
+ // be any host reachable from the client using webscokets, at any port.
+ QString hostName = "localhost";
+ int port = 1515;
+ qDebug() << "This example connects to a server at" << hostName << "port" << port << ","
+ << "where it expects to find a WebSockify server, which forwards to the fortune server.";
+
+ auto echo = [hostName, port]() {
+ QTcpSocket *socket = new QTcpSocket();
+
+ QObject::connect(socket, &QAbstractSocket::connected, [socket]() {
+ qDebug() << "Connected";
+ socket->write("Hello, echo server!");
+ socket->flush();
+ });
+
+ QObject::connect(socket, &QIODevice::readyRead, [socket]() {
+ QByteArray data = socket->readAll();
+ qDebug() << "Ready Read, got echo:" << data;
+ socket->disconnectFromHost();
+ socket->deleteLater();
+ });
+
+ QObject::connect(socket, &QAbstractSocket::errorOccurred, [socket]() {
+ qDebug() << "Error Occurred" << socket->error();
+ });
+
+ QObject::connect(socket, &QAbstractSocket::disconnected, [socket]() {
+ qDebug() << "Disconnected";
+ socket->deleteLater();
+ });
+
+ qDebug() << "Connect to host" << hostName << port;
+ socket->connectToHost(hostName, port);
+ };
+
+ QTimer::singleShot(500, [echo](){
+ echo();
+ });
+
+ return app.exec();
+}
diff --git a/tests/manual/wasm/network/echo_client_secondarythread/CMakeLists.txt b/tests/manual/wasm/network/echo_client_secondarythread/CMakeLists.txt
new file mode 100644
index 0000000000..a1f2bef254
--- /dev/null
+++ b/tests/manual/wasm/network/echo_client_secondarythread/CMakeLists.txt
@@ -0,0 +1,8 @@
+qt_internal_add_manual_test(echo_client_secondarythread
+ GUI
+ SOURCES
+ main.cpp
+ LIBRARIES
+ Qt::Core
+ Qt::Network
+)
diff --git a/tests/manual/wasm/network/echo_client_secondarythread/main.cpp b/tests/manual/wasm/network/echo_client_secondarythread/main.cpp
new file mode 100644
index 0000000000..52cea93495
--- /dev/null
+++ b/tests/manual/wasm/network/echo_client_secondarythread/main.cpp
@@ -0,0 +1,50 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#include <QtCore>
+#include <QtNetwork>
+
+int main(int argc, char **argv) {
+
+ QCoreApplication app(argc, argv);
+
+ // This example connects to localhost, but note that the host can
+ // be any host reachable from the client using webscokets, at any port.
+ QString hostName = "localhost";
+ int port = 1515;
+ qDebug() << "## This example connects to a server at" << hostName << "port" << port << ","
+ << "where it expects to find a WebSockify server, which forwards to the fortune server.";
+
+ auto echo = [hostName, port]() {
+ qDebug() << "Connecting to" << hostName << port;
+
+ QTcpSocket socket;
+ socket.connectToHost(hostName, port);
+ bool connected = socket.waitForConnected(3000);
+ if (!connected) {
+ qDebug() << "connect failure";
+ return;
+ }
+
+ qDebug() << "Connected";
+ socket.write("echo:Hello, echo server!;");
+ socket.flush();
+
+ qDebug() << "Calling waitForReadyRead()";
+ socket.waitForReadyRead(20000);
+ QByteArray data = socket.readAll();
+ qDebug() << "Got echo:" << data;
+
+ socket.disconnectFromHost();
+ socket.deleteLater();
+ qDebug() << "Disconnected";
+ };
+
+ QThread thread;
+ QObject::connect(&thread, &QThread::started, [echo](){
+ echo();
+ });
+ thread.start();
+
+ app.exec();
+}
diff --git a/tests/manual/wasm/network/echo_server/CMakeLists.txt b/tests/manual/wasm/network/echo_server/CMakeLists.txt
new file mode 100644
index 0000000000..cf98163fb8
--- /dev/null
+++ b/tests/manual/wasm/network/echo_server/CMakeLists.txt
@@ -0,0 +1,14 @@
+project(echo_server)
+cmake_minimum_required(VERSION 3.19)
+
+find_package(Qt6 COMPONENTS Core)
+find_package(Qt6 COMPONENTS network)
+
+qt_add_executable(echo_server
+ main.cpp
+)
+
+target_link_libraries(echo_server PUBLIC
+ Qt::Core
+ Qt::Network
+)
diff --git a/tests/manual/wasm/network/echo_server/main.cpp b/tests/manual/wasm/network/echo_server/main.cpp
new file mode 100644
index 0000000000..3a67cabc79
--- /dev/null
+++ b/tests/manual/wasm/network/echo_server/main.cpp
@@ -0,0 +1,80 @@
+// Copyright (C) 2021 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#include <QtCore>
+#include <QtNetwork>
+
+const int timeout = 60 * 1000;
+
+int main(int argc, char **argv)
+{
+ QCoreApplication app(argc, argv);
+
+ QTcpServer server;
+ QObject::connect(&server, &QTcpServer::newConnection, [&server](){
+ qDebug() << "new connection";
+
+ QByteArray *receiveBuffer = new QByteArray();
+
+ QTcpSocket *socket = server.nextPendingConnection();
+ QObject::connect(socket, &QIODevice::readyRead, [socket, receiveBuffer](){
+
+ // This implements a very simple command protocol, where the server
+ // processes a stream of commands delimited by ';', and then performs
+ // an action in reply. The supported commands with actions are:
+ //
+ // echo:<message>; writes the received <message> back
+ // close; closes the socket
+ //
+
+ // We might receive multiple or partial commands; read all available data
+ // and then scan the buffer for complete commands.
+ QByteArray newData = socket->readAll();
+ *receiveBuffer += newData;
+
+ int pos = receiveBuffer->indexOf(";");
+ while (pos != -1) {
+ QByteArray command = receiveBuffer->left(pos);
+ receiveBuffer->remove(0, pos + 1);
+ pos = receiveBuffer->indexOf(";");
+
+ if (command.startsWith("echo")) {
+ // Echo expects echo:<message>
+ QList<QByteArray> parts = command.split(':');
+ QByteArray reply = parts.last() + ';';
+ qDebug() << "Command: echo:" << parts.last();
+ socket->write(reply);
+ socket->flush();
+
+ } else if (command.startsWith("close")) {
+ qDebug() << "Command: close";
+ socket->write("bye!;");
+ socket->flush();
+ socket->close();
+ break;
+ } else {
+ qDebug() << "Unknown command:" << command;
+ }
+ }
+ });
+
+ QObject::connect(socket, &QAbstractSocket::disconnected, [socket, receiveBuffer](){
+ delete receiveBuffer;
+ socket->deleteLater();
+ });
+ });
+
+ // This is example is intended to be used together with WebSockify on
+ // the server and acts as a counterpart to the client examples which
+ // run in the browser. (This example does not run in the browser).
+
+ qDebug() << "\nStarting echo server at port 1516. You should now start the"
+ << "\nWebSockify forwarding server, and then connect from one of"
+ << "\nthe client examples."
+ << "\n websockify 1515 localhost:1516";
+
+ server.listen(QHostAddress::Any, 1516);
+
+ return app.exec();
+}
+
diff --git a/tests/manual/wasm/network/sockify_sockets_auto/CMakeLists.txt b/tests/manual/wasm/network/sockify_sockets_auto/CMakeLists.txt
new file mode 100644
index 0000000000..fb9a9f8543
--- /dev/null
+++ b/tests/manual/wasm/network/sockify_sockets_auto/CMakeLists.txt
@@ -0,0 +1,22 @@
+qt_internal_add_manual_test(sockify_sockets_auto
+ SOURCES
+ main.cpp
+ ../../qtwasmtestlib/qtwasmtestlib.cpp
+ LIBRARIES
+ Qt::Core
+ Qt::Network
+)
+
+include_directories(../../qtwasmtestlib/)
+
+add_custom_command(
+ TARGET sockify_sockets_auto POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E copy
+ ${CMAKE_CURRENT_SOURCE_DIR}/sockify_sockets_auto.html
+ ${CMAKE_CURRENT_BINARY_DIR}/sockify_sockets_auto.html)
+
+add_custom_command(
+ TARGET sockify_sockets_auto POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E copy
+ ${CMAKE_CURRENT_SOURCE_DIR}/../../qtwasmtestlib/qtwasmtestlib.js
+ ${CMAKE_CURRENT_BINARY_DIR}/qtwasmtestlib.js)
diff --git a/tests/manual/wasm/network/sockify_sockets_auto/main.cpp b/tests/manual/wasm/network/sockify_sockets_auto/main.cpp
new file mode 100644
index 0000000000..b6aa232b4a
--- /dev/null
+++ b/tests/manual/wasm/network/sockify_sockets_auto/main.cpp
@@ -0,0 +1,318 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#include <qtwasmtestlib.h>
+#include <QtCore>
+#include <QtNetwork>
+
+const int socketWait = 1000;
+const QString hostName = "localhost";
+const int port = 1515;
+
+class SockifySocketsTest: public QObject
+{
+ Q_OBJECT
+
+private slots:
+ void echo();
+ void echoMultipleMessages();
+ void echoMultipleSockets();
+ void remoteClose();
+
+#if QT_CONFIG(thread)
+ void thread_echo();
+ void thread_remoteClose();
+ void thread_echoMultipleSockets();
+#endif
+
+#ifdef QT_HAVE_EMSCRIPTEN_ASYNCIFY
+ void asyncify_echo();
+ void asyncify_remoteClose();
+#endif
+};
+
+class CompleteTestFunctionRefGuard {
+public:
+ CompleteTestFunctionRefGuard(CompleteTestFunctionRefGuard const&) = delete;
+ CompleteTestFunctionRefGuard& operator=(CompleteTestFunctionRefGuard const&) = delete;
+
+ static CompleteTestFunctionRefGuard *create() {
+ return new CompleteTestFunctionRefGuard();
+ }
+
+ void ref() {
+ QMutexLocker lock(&mutex);
+ ++counter;
+ }
+
+ void deref() {
+ bool itsTheFinalDeref = [this] {
+ QMutexLocker lock(&mutex);
+ return --counter == 0;
+ }();
+
+ if (itsTheFinalDeref) {
+ delete this;
+ QtWasmTest::completeTestFunction();
+ }
+ }
+private:
+ CompleteTestFunctionRefGuard() { };
+
+ QMutex mutex;
+ int counter = 0;
+};
+
+#if QT_CONFIG(thread)
+
+class TestThread : public QThread
+{
+public:
+ static QThread *create(std::function<void()> started, std::function<void()> finished)
+ {
+ TestThread *thread = new TestThread();
+ connect(thread, &QThread::started, [started]() {
+ started();
+ });
+ connect(thread, &QThread::finished, [thread, finished]() {
+ finished();
+ thread->deleteLater();
+ });
+ thread->start();
+ return thread;
+ }
+};
+
+#endif
+
+void blockingEchoTest()
+{
+ QTcpSocket socket;
+ socket.connectToHost(hostName, port);
+ if (!socket.waitForConnected(socketWait))
+ qFatal("socket connect error");
+
+ QByteArray message = "Hello, echo server!";
+
+ QByteArray command = "echo:" + message + ';';
+ socket.write(command);
+ socket.flush();
+
+ socket.waitForReadyRead(socketWait);
+ QByteArray expectedReply = message + ';';
+ QByteArray reply = socket.readAll();
+ if (reply != expectedReply)
+ qFatal("echo_multiple received incorrect reply");
+ socket.disconnectFromHost();
+}
+
+void blockingRemoteClose()
+{
+ QTcpSocket socket;
+
+ qDebug() << "## connectToHost";
+ socket.connectToHost(hostName, port);
+
+ qDebug() << "## waitForConnected";
+ socket.waitForConnected(socketWait);
+ socket.write("close;");
+ socket.flush();
+
+ qDebug() << "## waitForBytesWritten";
+ socket.waitForBytesWritten(socketWait);
+
+ qDebug() << "## waitForReadyRead";
+ socket.waitForReadyRead(200);
+
+ qDebug() << "## waitForDisconnected";
+ socket.waitForDisconnected(socketWait);
+ qDebug() << "## done";
+}
+
+// Verify that sending one echo command and receiving the reply works
+void SockifySocketsTest::echo()
+{
+ QTcpSocket *socket = new QTcpSocket();
+ socket->connectToHost(hostName, port);
+
+ QByteArray message = "Hello, echo server!";
+
+ QObject::connect(socket, &QAbstractSocket::connected, [socket, message]() {
+ QByteArray command = "echo:" + message + ';';
+ socket->write(command);
+ socket->flush();
+ });
+
+ QByteArray *reply = new QByteArray();
+ QObject::connect(socket, &QIODevice::readyRead, [socket, reply, message]() {
+ *reply += socket->readAll();
+ if (reply->contains(';')) {
+ bool match = (*reply == message + ';');
+ socket->disconnectFromHost();
+ socket->deleteLater();
+ delete reply;
+ QtWasmTest::completeTestFunction(match ? QtWasmTest::TestResult::Pass : QtWasmTest::TestResult::Fail, std::string());
+ }
+ });
+}
+
+void SockifySocketsTest::echoMultipleMessages()
+{
+ const int count = 20;
+
+ QTcpSocket *socket = new QTcpSocket();
+ socket->connectToHost(hostName, port);
+ QByteArray message = "Hello, echo server!";
+
+ QObject::connect(socket, &QAbstractSocket::connected, [socket, message]() {
+ QByteArray command = "echo:" + message + ';';
+ for (int i = 0; i < count; ++i) {
+ quint64 written = socket->write(command);
+ if (written != quint64(command.size()))
+ qFatal("Unable to write to socket");
+ }
+ socket->flush();
+ });
+
+ QByteArray expectedReply;
+ for (int i = 0; i < count; ++i)
+ expectedReply += (message + ';');
+ QByteArray *receivedReply = new QByteArray;
+ QObject::connect(socket, &QIODevice::readyRead, [socket, receivedReply, expectedReply]() {
+ QByteArray reply = socket->readAll();
+ *receivedReply += reply;
+
+ if (*receivedReply == expectedReply) {
+ socket->disconnectFromHost();
+ socket->deleteLater();
+ delete receivedReply;
+ QtWasmTest::completeTestFunction();
+ }
+ });
+}
+
+void SockifySocketsTest::echoMultipleSockets()
+{
+ const int connections = 5;
+ auto guard = CompleteTestFunctionRefGuard::create();
+
+ QByteArray message = "Hello, echo server!";
+
+ for (int i = 0; i < connections; ++i) {
+ guard->ref();
+
+ QTcpSocket *socket = new QTcpSocket();
+ socket->connectToHost(hostName, port);
+
+ QObject::connect(socket, &QAbstractSocket::connected, [socket, message]() {
+ QByteArray command = "echo:" + message + ';';
+ socket->write(command);
+ socket->flush();
+ });
+
+ QObject::connect(socket, &QIODevice::readyRead, [guard, socket, message]() {
+ QByteArray reply = socket->readAll();
+ socket->disconnectFromHost();
+ socket->deleteLater();
+ if (reply != (message + ';'))
+ qFatal("echo_multiple received incorrect reply");
+ guard->deref();
+ });
+ }
+}
+
+void SockifySocketsTest::remoteClose()
+{
+ QTcpSocket *socket = new QTcpSocket();
+ socket->connectToHost(hostName, port);
+ QObject::connect(socket, &QAbstractSocket::connected, [socket]() {
+ socket->write("close;");
+ socket->flush();
+ });
+ QObject::connect(socket, &QAbstractSocket::disconnected, [socket]() {
+ qDebug() << "disconnected";
+ socket->deleteLater();
+ QtWasmTest::completeTestFunction();
+ });
+}
+
+#if QT_CONFIG(thread)
+
+void SockifySocketsTest::thread_echo()
+{
+ auto started = []() {
+ blockingEchoTest();
+ QThread::currentThread()->quit();
+ };
+
+ auto finished = [](){
+ QtWasmTest::completeTestFunction();
+ };
+
+ TestThread::create(started, finished);
+}
+
+void SockifySocketsTest::thread_echoMultipleSockets()
+{
+ const int connections = 2; // TODO: test more threads
+ auto guard = CompleteTestFunctionRefGuard::create();
+ guard->ref();
+
+ for (int i = 0; i < connections; ++i) {
+ guard->ref();
+ auto started = [](){
+ blockingEchoTest();
+ QThread::currentThread()->quit();
+ };
+
+ auto finished = [guard](){
+ guard->deref();
+ };
+
+ TestThread::create(started, finished);
+ }
+
+ guard->deref();
+}
+
+void SockifySocketsTest::thread_remoteClose()
+{
+ auto started = [](){
+ blockingRemoteClose();
+ QThread::currentThread()->quit();
+ };
+
+ auto finished = [](){
+ QtWasmTest::completeTestFunction();
+ };
+
+ TestThread::create(started, finished);
+}
+
+#endif
+
+#ifdef QT_HAVE_EMSCRIPTEN_ASYNCIFY
+
+// Post an event to the main thread and asyncify wait for it
+void SockifySocketsTest::asyncify_echo()
+{
+ blockingEchoTest();
+ QtWasmTest::completeTestFunction();
+}
+
+void SockifySocketsTest::asyncify_remoteClose()
+{
+ blockingRemoteClose();
+ QtWasmTest::completeTestFunction();
+}
+
+#endif
+
+int main(int argc, char **argv)
+{
+ auto testObject = std::make_shared<SockifySocketsTest>();
+ QtWasmTest::initTestCase<QCoreApplication>(argc, argv, testObject);
+ return 0;
+}
+
+#include "main.moc"
diff --git a/tests/manual/wasm/network/sockify_sockets_auto/sockify_sockets_auto.html b/tests/manual/wasm/network/sockify_sockets_auto/sockify_sockets_auto.html
new file mode 100644
index 0000000000..080ada94e7
--- /dev/null
+++ b/tests/manual/wasm/network/sockify_sockets_auto/sockify_sockets_auto.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<script type="text/javascript" src="qtwasmtestlib.js"></script>
+<script type="text/javascript" src="sockify_sockets_auto.js"></script>
+<script>
+ window.onload = async () => {
+ runTestCase(sockify_sockets_auto_entry, document.getElementById("log"));
+ };
+</script>
+<p> Sockify tunneled sockets auto test.
+
+<p>This test requires running echo_server and <a href=https://github.com/novnc/websockify>websockify</a> (or equivalent) on the host:
+<pre>
+ /path/to/qtbase/tests/manual/wasm/network/echo_server/echo_server
+ websockify 1515 localhost:1516
+</pre>
+
+<div id="log"></div>
diff --git a/tests/manual/wasm/qstdweb/CMakeLists.txt b/tests/manual/wasm/qstdweb/CMakeLists.txt
new file mode 100644
index 0000000000..5242999ec4
--- /dev/null
+++ b/tests/manual/wasm/qstdweb/CMakeLists.txt
@@ -0,0 +1,97 @@
+qt_internal_add_manual_test(promise_auto
+ SOURCES
+ promise_main.cpp
+ ../qtwasmtestlib/qtwasmtestlib.cpp
+ LIBRARIES
+ Qt::Core
+ Qt::CorePrivate
+)
+
+include_directories(../qtwasmtestlib/)
+
+add_custom_command(
+ TARGET promise_auto POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E copy
+ ${CMAKE_CURRENT_SOURCE_DIR}/promise_auto.html
+ ${CMAKE_CURRENT_BINARY_DIR}/promise_auto.html)
+
+add_custom_command(
+ TARGET promise_auto POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E copy
+ ${CMAKE_CURRENT_SOURCE_DIR}/../qtwasmtestlib/qtwasmtestlib.js
+ ${CMAKE_CURRENT_BINARY_DIR}/qtwasmtestlib.js)
+
+qt_internal_add_manual_test(files_auto
+ SOURCES
+ files_main.cpp
+ ../qtwasmtestlib/qtwasmtestlib.cpp
+ LIBRARIES
+ Qt::Core
+ Qt::CorePrivate
+ Qt::GuiPrivate
+)
+
+include_directories(../qtwasmtestlib/)
+
+add_custom_command(
+ TARGET files_auto POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E copy
+ ${CMAKE_CURRENT_SOURCE_DIR}/files_auto.html
+ ${CMAKE_CURRENT_BINARY_DIR}/files_auto.html)
+
+add_custom_command(
+ TARGET files_auto POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E copy
+ ${CMAKE_CURRENT_SOURCE_DIR}/../qtwasmtestlib/qtwasmtestlib.js
+ ${CMAKE_CURRENT_BINARY_DIR}/qtwasmtestlib.js)
+
+qt_internal_add_manual_test(qwasmcompositor_auto
+ SOURCES
+ qwasmcompositor_main.cpp
+ ../qtwasmtestlib/qtwasmtestlib.cpp
+ LIBRARIES
+ Qt::Core
+ Qt::CorePrivate
+ Qt::GuiPrivate
+)
+
+include_directories(../qtwasmtestlib/)
+
+add_custom_command(
+ TARGET qwasmcompositor_auto POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E copy
+ ${CMAKE_CURRENT_SOURCE_DIR}/qwasmcompositor_auto.html
+ ${CMAKE_CURRENT_BINARY_DIR}/qwasmcompositor_auto.html)
+
+add_custom_command(
+ TARGET qwasmcompositor_auto POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E copy
+ ${CMAKE_CURRENT_SOURCE_DIR}/../qtwasmtestlib/qtwasmtestlib.js
+ ${CMAKE_CURRENT_BINARY_DIR}/qtwasmtestlib.js)
+
+target_link_options(qwasmcompositor_auto PRIVATE -sASYNCIFY -Os)
+
+qt_internal_add_manual_test(iodevices_auto
+ SOURCES
+ iodevices_main.cpp
+ ../qtwasmtestlib/qtwasmtestlib.cpp
+ LIBRARIES
+ Qt::Core
+ Qt::CorePrivate
+ Qt::GuiPrivate
+)
+
+add_custom_command(
+ TARGET iodevices_auto POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E copy
+ ${CMAKE_CURRENT_SOURCE_DIR}/iodevices_auto.html
+ ${CMAKE_CURRENT_BINARY_DIR}/iodevices_auto.html)
+
+add_custom_command(
+ TARGET iodevices_auto POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E copy
+ ${CMAKE_CURRENT_SOURCE_DIR}/../qtwasmtestlib/qtwasmtestlib.js
+ ${CMAKE_CURRENT_BINARY_DIR}/qtwasmtestlib.js)
+
+target_link_options(iodevices_auto PRIVATE -sASYNCIFY -Os)
+
diff --git a/tests/manual/wasm/qstdweb/files_auto.html b/tests/manual/wasm/qstdweb/files_auto.html
new file mode 100644
index 0000000000..9027fdc660
--- /dev/null
+++ b/tests/manual/wasm/qstdweb/files_auto.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<script type="text/javascript" src="https://sinonjs.org/releases/sinon-14.0.0.js"
+ integrity="sha384-z8J4N1s2hPDn6ClmFXDQkKD/e738VOWcR8JmhztPRa+PgezxQupgZu3LzoBO4Jw8"
+ crossorigin="anonymous"></script>
+<script type="text/javascript" src="qtwasmtestlib.js"></script>
+<script type="text/javascript" src="files_auto.js"></script>
+<script>
+ window.onload = () => {
+ runTestCase(files_auto_entry, document.getElementById("log"));
+ };
+</script>
+<p>Running files auto test.</p>
+<div id="log"></div>
diff --git a/tests/manual/wasm/qstdweb/files_main.cpp b/tests/manual/wasm/qstdweb/files_main.cpp
new file mode 100644
index 0000000000..45939feb10
--- /dev/null
+++ b/tests/manual/wasm/qstdweb/files_main.cpp
@@ -0,0 +1,471 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#include <QtCore/QCoreApplication>
+#include <QtCore/QEvent>
+#include <QtCore/QMutex>
+#include <QtCore/QObject>
+#include <QtCore/QTimer>
+#include <QtGui/private/qwasmlocalfileaccess_p.h>
+
+#include <qtwasmtestlib.h>
+#include <emscripten.h>
+#include <emscripten/bind.h>
+#include <emscripten/val.h>
+
+#include <string_view>
+
+using namespace emscripten;
+
+class FilesTest : public QObject
+{
+ Q_OBJECT
+
+public:
+ FilesTest() : m_window(val::global("window")), m_testSupport(val::object()) {}
+
+ ~FilesTest() noexcept {
+ for (auto& cleanup: m_cleanup) {
+ cleanup();
+ }
+ }
+
+private:
+ void init() {
+ EM_ASM({
+ window.testSupport = {};
+
+ window.showOpenFilePicker = sinon.stub();
+
+ window.mockOpenFileDialog = (files) => {
+ window.showOpenFilePicker.withArgs(sinon.match.any).callsFake(
+ (options) => Promise.resolve(files.map(file => {
+ const getFile = sinon.stub();
+ getFile.callsFake(() => Promise.resolve({
+ name: file.name,
+ size: file.content.length,
+ slice: () => new Blob([new TextEncoder().encode(file.content)]),
+ }));
+ return {
+ kind: 'file',
+ name: file.name,
+ getFile
+ };
+ }))
+ );
+ };
+
+ window.showSaveFilePicker = sinon.stub();
+
+ window.mockSaveFilePicker = (file) => {
+ window.showSaveFilePicker.withArgs(sinon.match.any).callsFake(
+ (options) => {
+ const createWritable = sinon.stub();
+ createWritable.callsFake(() => {
+ const write = file.writeFn ?? (() => {
+ const write = sinon.stub();
+ write.callsFake((stuff) => {
+ if (file.content !== new TextDecoder().decode(stuff)) {
+ const message = `Bad file content ${file.content} !== ${new TextDecoder().decode(stuff)}`;
+ Module.qtWasmFail(message);
+ return Promise.reject(message);
+ }
+
+ return Promise.resolve();
+ });
+ return write;
+ })();
+
+ window.testSupport.write = write;
+
+ const close = file.closeFn ?? (() => {
+ const close = sinon.stub();
+ close.callsFake(() => Promise.resolve());
+ return close;
+ })();
+
+ window.testSupport.close = close;
+
+ return Promise.resolve({
+ write,
+ close
+ });
+ });
+ return Promise.resolve({
+ kind: 'file',
+ name: file.name,
+ createWritable
+ });
+ }
+ );
+ };
+ });
+ }
+
+ template <class T>
+ T* Own(T* plainPtr) {
+ m_cleanup.emplace_back([plainPtr]() mutable {
+ delete plainPtr;
+ });
+ return plainPtr;
+ }
+
+ val m_window;
+ val m_testSupport;
+
+ std::vector<std::function<void()>> m_cleanup;
+
+private slots:
+ void selectOneFileWithFileDialog();
+ void selectMultipleFilesWithFileDialog();
+ void cancelFileDialog();
+ void rejectFile();
+ void saveFileWithFileDialog();
+};
+
+class BarrierCallback {
+public:
+ BarrierCallback(int number, std::function<void()> onDone)
+ : m_remaining(number), m_onDone(std::move(onDone)) {}
+
+ void operator()() {
+ if (!--m_remaining) {
+ m_onDone();
+ }
+ }
+
+private:
+ int m_remaining;
+ std::function<void()> m_onDone;
+};
+
+
+template <class Arg>
+std::string argToString(std::add_lvalue_reference_t<std::add_const_t<Arg>> arg) {
+ return std::to_string(arg);
+}
+
+template <>
+std::string argToString<bool>(const bool& value) {
+ return value ? "true" : "false";
+}
+
+template <>
+std::string argToString<std::string>(const std::string& arg) {
+ return arg;
+}
+
+template <>
+std::string argToString<const std::string&>(const std::string& arg) {
+ return arg;
+}
+
+template<class Type>
+struct Matcher {
+ virtual ~Matcher() = default;
+
+ virtual bool matches(std::string* explanation, const Type& actual) const = 0;
+};
+
+template<class Type>
+struct AnyMatcher : public Matcher<Type> {
+ bool matches(std::string* explanation, const Type& actual) const final {
+ Q_UNUSED(explanation);
+ Q_UNUSED(actual);
+ return true;
+ }
+
+ Type m_value;
+};
+
+template<class Type>
+struct EqualsMatcher : public Matcher<Type> {
+ EqualsMatcher(Type value) : m_value(std::forward<Type>(value)) {}
+
+ bool matches(std::string* explanation, const Type& actual) const final {
+ const bool ret = actual == m_value;
+ if (!ret)
+ *explanation += argToString<Type>(actual) + " != " + argToString<Type>(m_value);
+ return actual == m_value;
+ }
+
+ // It is crucial to hold a copy, otherwise we lose const refs.
+ std::remove_reference_t<Type> m_value;
+};
+
+template<class Type>
+std::unique_ptr<EqualsMatcher<Type>> equals(Type value) {
+ return std::make_unique<EqualsMatcher<Type>>(value);
+}
+
+template<class Type>
+std::unique_ptr<AnyMatcher<Type>> any(Type value) {
+ return std::make_unique<AnyMatcher<Type>>(value);
+}
+
+template <class ...Types>
+struct Expectation {
+ std::tuple<std::unique_ptr<Matcher<Types>>...> m_argMatchers;
+ int m_callCount = 0;
+ int m_expectedCalls = 1;
+
+ template<std::size_t... Indices>
+ bool match(std::string* explanation, const std::tuple<Types...>& tuple, std::index_sequence<Indices...>) const {
+ return ( ... && (std::get<Indices>(m_argMatchers)->matches(explanation, std::get<Indices>(tuple))));
+ }
+
+ bool matches(std::string* explanation, Types... args) const {
+ if (m_callCount >= m_expectedCalls) {
+ *explanation += "Too many calls\n";
+ return false;
+ }
+ return match(explanation, std::make_tuple(args...), std::make_index_sequence<std::tuple_size_v<std::tuple<Types...>>>());
+ }
+};
+
+template <class R, class ...Types>
+struct Behavior {
+ std::function<R(Types...)> m_callback;
+
+ void call(std::function<R(Types...)> callback) {
+ m_callback = std::move(callback);
+ }
+};
+
+template<class... Args>
+std::string argsToString(Args... args) {
+ return (... + (", " + argToString<Args>(args)));
+}
+
+template<>
+std::string argsToString<>() {
+ return "";
+}
+
+template<class R, class ...Types>
+struct ExpectationToBehaviorMapping {
+ Expectation<Types...> expectation;
+ Behavior<R, Types...> behavior;
+};
+
+template<class R, class... Args>
+class MockCallback {
+public:
+ std::function<R(Args...)> get() {
+ return [this](Args... result) -> R {
+ return processCall(std::forward<Args>(result)...);
+ };
+ }
+
+ Behavior<R, Args...>& expectCallWith(std::unique_ptr<Matcher<Args>>... matcherArgs) {
+ auto matchers = std::make_tuple(std::move(matcherArgs)...);
+ m_behaviorByExpectation.push_back({Expectation<Args...> {std::move(matchers)}, Behavior<R, Args...> {}});
+ return m_behaviorByExpectation.back().behavior;
+ }
+
+ Behavior<R, Args...>& expectRepeatedCallWith(int times, std::unique_ptr<Matcher<Args>>... matcherArgs) {
+ auto matchers = std::make_tuple(std::move(matcherArgs)...);
+ m_behaviorByExpectation.push_back({Expectation<Args...> {std::move(matchers), 0, times}, Behavior<R, Args...> {}});
+ return m_behaviorByExpectation.back().behavior;
+ }
+
+private:
+ R processCall(Args... args) {
+ std::string argsAsString = argsToString(args...);
+ std::string triedExpectations;
+ auto it = std::find_if(m_behaviorByExpectation.begin(), m_behaviorByExpectation.end(),
+ [&](const ExpectationToBehaviorMapping<R, Args...>& behavior) {
+ return behavior.expectation.matches(&triedExpectations, std::forward<Args>(args)...);
+ });
+ if (it != m_behaviorByExpectation.end()) {
+ ++it->expectation.m_callCount;
+ return it->behavior.m_callback(args...);
+ } else {
+ QWASMFAIL("Unexpected call with " + argsAsString + ". Tried: " + triedExpectations);
+ }
+ return R();
+ }
+
+ std::vector<ExpectationToBehaviorMapping<R, Args...>> m_behaviorByExpectation;
+};
+
+void FilesTest::selectOneFileWithFileDialog()
+{
+ init();
+
+ static constexpr std::string_view testFileContent = "This is a happy case.";
+
+ EM_ASM({
+ mockOpenFileDialog([{
+ name: 'file1.jpg',
+ content: UTF8ToString($0)
+ }]);
+ }, testFileContent.data());
+
+ auto* fileSelectedCallback = Own(new MockCallback<void, bool>());
+ fileSelectedCallback->expectCallWith(equals(true)).call([](bool) mutable {});
+
+ auto* fileBuffer = Own(new QByteArray());
+
+ auto* acceptFileCallback = Own(new MockCallback<char*, uint64_t, const std::string&>());
+ acceptFileCallback->expectCallWith(equals<uint64_t>(testFileContent.size()), equals<const std::string&>("file1.jpg"))
+ .call([fileBuffer](uint64_t, std::string) mutable -> char* {
+ fileBuffer->resize(testFileContent.size());
+ return fileBuffer->data();
+ });
+
+ auto* fileDataReadyCallback = Own(new MockCallback<void>());
+ fileDataReadyCallback->expectCallWith().call([fileBuffer]() mutable {
+ QWASMCOMPARE(fileBuffer->data(), std::string(testFileContent));
+ QWASMSUCCESS();
+ });
+
+ QWasmLocalFileAccess::openFile("*", fileSelectedCallback->get(), acceptFileCallback->get(),
+ fileDataReadyCallback->get());
+}
+
+void FilesTest::selectMultipleFilesWithFileDialog()
+{
+ static constexpr std::array<std::string_view, 3> testFileContent =
+ { "Cont 1", "2s content", "What is hiding in 3?"};
+
+ init();
+
+ EM_ASM({
+ mockOpenFileDialog([{
+ name: 'file1.jpg',
+ content: UTF8ToString($0)
+ }, {
+ name: 'file2.jpg',
+ content: UTF8ToString($1)
+ }, {
+ name: 'file3.jpg',
+ content: UTF8ToString($2)
+ }]);
+ }, testFileContent[0].data(), testFileContent[1].data(), testFileContent[2].data());
+
+ auto* fileSelectedCallback = Own(new MockCallback<void, int>());
+ fileSelectedCallback->expectCallWith(equals(3)).call([](int) mutable {});
+
+ auto fileBuffer = std::make_shared<QByteArray>();
+
+ auto* acceptFileCallback = Own(new MockCallback<char*, uint64_t, const std::string&>());
+ acceptFileCallback->expectCallWith(equals<uint64_t>(testFileContent[0].size()), equals<const std::string&>("file1.jpg"))
+ .call([fileBuffer](uint64_t, std::string) mutable -> char* {
+ fileBuffer->resize(testFileContent[0].size());
+ return fileBuffer->data();
+ });
+ acceptFileCallback->expectCallWith(equals<uint64_t>(testFileContent[1].size()), equals<const std::string&>("file2.jpg"))
+ .call([fileBuffer](uint64_t, std::string) mutable -> char* {
+ fileBuffer->resize(testFileContent[1].size());
+ return fileBuffer->data();
+ });
+ acceptFileCallback->expectCallWith(equals<uint64_t>(testFileContent[2].size()), equals<const std::string&>("file3.jpg"))
+ .call([fileBuffer](uint64_t, std::string) mutable -> char* {
+ fileBuffer->resize(testFileContent[2].size());
+ return fileBuffer->data();
+ });
+
+ auto* fileDataReadyCallback = Own(new MockCallback<void>());
+ fileDataReadyCallback->expectRepeatedCallWith(3).call([fileBuffer]() mutable {
+ static int callCount = 0;
+ QWASMCOMPARE(fileBuffer->data(), std::string(testFileContent[callCount]));
+
+ callCount++;
+ if (callCount == 3) {
+ QWASMSUCCESS();
+ }
+ });
+
+ QWasmLocalFileAccess::openFiles("*", QWasmLocalFileAccess::FileSelectMode::MultipleFiles,
+ fileSelectedCallback->get(), acceptFileCallback->get(),
+ fileDataReadyCallback->get());
+}
+
+void FilesTest::cancelFileDialog()
+{
+ init();
+
+ EM_ASM({
+ window.showOpenFilePicker.withArgs(sinon.match.any).returns(Promise.reject("The user cancelled the dialog"));
+ });
+
+ auto* fileSelectedCallback = Own(new MockCallback<void, bool>());
+ fileSelectedCallback->expectCallWith(equals(false)).call([](bool) mutable {
+ QWASMSUCCESS();
+ });
+
+ auto* acceptFileCallback = Own(new MockCallback<char*, uint64_t, const std::string&>());
+ auto* fileDataReadyCallback = Own(new MockCallback<void>());
+
+ QWasmLocalFileAccess::openFile("*", fileSelectedCallback->get(), acceptFileCallback->get(),
+ fileDataReadyCallback->get());
+}
+
+void FilesTest::rejectFile()
+{
+ init();
+
+ static constexpr std::string_view testFileContent = "We don't want this file.";
+
+ EM_ASM({
+ mockOpenFileDialog([{
+ name: 'dontwant.dat',
+ content: UTF8ToString($0)
+ }]);
+ }, testFileContent.data());
+
+ auto* fileSelectedCallback = Own(new MockCallback<void, bool>());
+ fileSelectedCallback->expectCallWith(equals(true)).call([](bool) mutable {});
+
+ auto* fileDataReadyCallback = Own(new MockCallback<void>());
+
+ auto* acceptFileCallback = Own(new MockCallback<char*, uint64_t, const std::string&>());
+ acceptFileCallback->expectCallWith(equals<uint64_t>(std::string_view(testFileContent).size()), equals<const std::string&>("dontwant.dat"))
+ .call([](uint64_t, const std::string) {
+ QTimer::singleShot(0, []() {
+ // No calls to fileDataReadyCallback
+ QWASMSUCCESS();
+ });
+ return nullptr;
+ });
+
+ QWasmLocalFileAccess::openFile("*", fileSelectedCallback->get(), acceptFileCallback->get(),
+ fileDataReadyCallback->get());
+}
+
+void FilesTest::saveFileWithFileDialog()
+{
+ init();
+
+ static constexpr std::string_view testFileContent = "Save this important content";
+
+ EM_ASM({
+ mockSaveFilePicker({
+ name: 'somename',
+ content: UTF8ToString($0),
+ closeFn: (() => {
+ const close = sinon.stub();
+ close.callsFake(() =>
+ new Promise(resolve => {
+ resolve();
+ Module.qtWasmPass();
+ }));
+ return close;
+ })()
+ });
+ }, testFileContent.data());
+
+ QByteArray data;
+ data.prepend(testFileContent);
+ QWasmLocalFileAccess::saveFile(data, "hintie");
+}
+
+int main(int argc, char **argv)
+{
+ auto testObject = std::make_shared<FilesTest>();
+ QtWasmTest::initTestCase<QCoreApplication>(argc, argv, testObject);
+ return 0;
+}
+
+#include "files_main.moc"
diff --git a/tests/manual/wasm/qstdweb/iodevices_auto.html b/tests/manual/wasm/qstdweb/iodevices_auto.html
new file mode 100644
index 0000000000..7937b8a483
--- /dev/null
+++ b/tests/manual/wasm/qstdweb/iodevices_auto.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<script type="text/javascript" src="qtwasmtestlib.js"></script>
+<script type="text/javascript" src="iodevices_auto.js"></script>
+<script>
+ window.onload = () => {
+ runTestCase(iodevices_auto_entry, document.getElementById("log"));
+ };
+</script>
+<p>Running qstdweb iodevices auto test.</p>
+<div id="log"></div>
diff --git a/tests/manual/wasm/qstdweb/iodevices_main.cpp b/tests/manual/wasm/qstdweb/iodevices_main.cpp
new file mode 100644
index 0000000000..0dbdd0084e
--- /dev/null
+++ b/tests/manual/wasm/qstdweb/iodevices_main.cpp
@@ -0,0 +1,103 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#include <QtCore/QtCore>
+#include <QtCore/private/qstdweb_p.h>
+
+#include <qtwasmtestlib.h>
+
+#include "emscripten.h"
+
+using qstdweb::ArrayBuffer;
+using qstdweb::Uint8Array;
+using qstdweb::Blob;
+using qstdweb::BlobIODevice;
+using qstdweb::Uint8ArrayIODevice;
+
+class WasmIoDevicesTest: public QObject
+{
+ Q_OBJECT
+
+private slots:
+ void blobIODevice();
+ void uint8ArrayIODevice();
+};
+
+// Creates a test arraybuffer with byte values [0..size] % 256 * 2
+char testByteValue(int i) { return (i % 256) * 2; }
+ArrayBuffer createTestArrayBuffer(int size)
+{
+ ArrayBuffer buffer(size);
+ Uint8Array array(buffer);
+ for (int i = 0; i < size; ++i)
+ array.val().set(i, testByteValue(i));
+ return buffer;
+}
+
+void WasmIoDevicesTest::blobIODevice()
+{
+ if (!qstdweb::canBlockCallingThread()) {
+ QtWasmTest::completeTestFunction(QtWasmTest::TestResult::Skip, "requires asyncify");
+ return;
+ }
+
+ // Create test buffer and BlobIODevice
+ const int bufferSize = 16;
+ BlobIODevice blobDevice(Blob::fromArrayBuffer(createTestArrayBuffer(bufferSize)));
+
+ // Read back byte for byte from the device
+ QWASMVERIFY(blobDevice.open(QIODevice::ReadOnly));
+ for (int i = 0; i < bufferSize; ++i) {
+ char byte;
+ blobDevice.seek(i);
+ blobDevice.read(&byte, 1);
+ QWASMCOMPARE(byte, testByteValue(i));
+ }
+
+ blobDevice.close();
+ QWASMVERIFY(!blobDevice.open(QIODevice::WriteOnly));
+ QWASMSUCCESS();
+}
+
+void WasmIoDevicesTest::uint8ArrayIODevice()
+{
+ // Create test buffer and Uint8ArrayIODevice
+ const int bufferSize = 1024;
+ Uint8Array array(createTestArrayBuffer(bufferSize));
+ Uint8ArrayIODevice arrayDevice(array);
+
+ // Read back byte for byte from the device
+ QWASMVERIFY(arrayDevice.open(QIODevice::ReadWrite));
+ for (int i = 0; i < bufferSize; ++i) {
+ char byte;
+ arrayDevice.seek(i);
+ arrayDevice.read(&byte, 1);
+ QWASMCOMPARE(byte, testByteValue(i));
+ }
+
+ // Write a different set of bytes
+ QWASMCOMPARE(arrayDevice.seek(0), true);
+ for (int i = 0; i < bufferSize; ++i) {
+ char byte = testByteValue(i + 1);
+ arrayDevice.seek(i);
+ QWASMCOMPARE(arrayDevice.write(&byte, 1), 1);
+ }
+
+ // Verify that the original array was updated
+ QByteArray copy = QByteArray::fromEcmaUint8Array(array.val());
+ for (int i = 0; i < bufferSize; ++i)
+ QWASMCOMPARE(copy.at(i), testByteValue(i + 1));
+
+ arrayDevice.close();
+ QWASMSUCCESS();
+}
+
+int main(int argc, char **argv)
+{
+ auto testObject = std::make_shared<WasmIoDevicesTest>();
+ QtWasmTest::initTestCase<QCoreApplication>(argc, argv, testObject);
+ return 0;
+}
+
+#include "iodevices_main.moc"
+
diff --git a/tests/manual/wasm/qstdweb/promise_auto.html b/tests/manual/wasm/qstdweb/promise_auto.html
new file mode 100644
index 0000000000..94a8dbb88a
--- /dev/null
+++ b/tests/manual/wasm/qstdweb/promise_auto.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<script type="text/javascript" src="qtwasmtestlib.js"></script>
+<script type="text/javascript" src="promise_auto.js"></script>
+<script>
+ window.onload = () => {
+ runTestCase(promise_auto_entry, document.getElementById("log"));
+ };
+</script>
+<p>Running promise auto test.</p>
+<div id="log"></div>
diff --git a/tests/manual/wasm/qstdweb/promise_main.cpp b/tests/manual/wasm/qstdweb/promise_main.cpp
new file mode 100644
index 0000000000..c5f6f7f412
--- /dev/null
+++ b/tests/manual/wasm/qstdweb/promise_main.cpp
@@ -0,0 +1,486 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#include <QtCore/QCoreApplication>
+#include <QtCore/QEvent>
+#include <QtCore/QMutex>
+#include <QtCore/QObject>
+#include <QtCore/private/qstdweb_p.h>
+
+#include <qtwasmtestlib.h>
+#include <emscripten.h>
+
+using namespace emscripten;
+
+class WasmPromiseTest : public QObject
+{
+ Q_OBJECT
+
+public:
+ WasmPromiseTest() : m_window(val::global("window")), m_testSupport(val::object()) {}
+
+ ~WasmPromiseTest() noexcept = default;
+
+private:
+ void init() {
+ m_testSupport = val::object();
+ m_window.set("testSupport", m_testSupport);
+
+ 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 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();
+};
+
+class BarrierCallback {
+public:
+ BarrierCallback(int number, std::function<void()> onDone)
+ : m_remaining(number), m_onDone(std::move(onDone)) {}
+
+ void operator()() {
+ if (!--m_remaining) {
+ m_onDone();
+ }
+ }
+
+private:
+ int m_remaining;
+ std::function<void()> m_onDone;
+};
+
+// Post event to the main thread and verify that it is processed.
+void WasmPromiseTest::simpleResolve()
+{
+ init();
+
+ qstdweb::Promise::make(m_testSupport, "makeTestPromise", {
+ .thenFunc = [](val result) {
+ QWASMVERIFY(result.isString());
+ QWASMCOMPARE("Some lovely data", result.as<std::string>());
+
+ QWASMSUCCESS();
+ },
+ .catchFunc = [](val error) {
+ Q_UNUSED(error);
+
+ QWASMFAIL("Unexpected catch");
+ }
+ }, std::string("simpleResolve"));
+
+ EM_ASM({
+ testSupport.resolve["simpleResolve"]("Some lovely data");
+ });
+}
+
+void WasmPromiseTest::multipleResolve()
+{
+ init();
+
+ static constexpr int promiseCount = 1000;
+
+ auto onThen = std::make_shared<BarrierCallback>(promiseCount, []() {
+ QWASMSUCCESS();
+ });
+
+ for (int i = 0; i < promiseCount; ++i) {
+ qstdweb::Promise::make(m_testSupport, "makeTestPromise", {
+ .thenFunc = [=](val result) {
+ QWASMVERIFY(result.isString());
+ QWASMCOMPARE(QString::number(i).toStdString(), result.as<std::string>());
+
+ (*onThen)();
+ },
+ .catchFunc = [](val error) {
+ Q_UNUSED(error);
+ QWASMFAIL("Unexpected catch");
+ }
+ }, (QStringLiteral("test") + QString::number(i)).toStdString());
+ }
+
+ EM_ASM({
+ for (let i = $0 - 1; i >= 0; --i) {
+ testSupport.resolve['test' + i](`${i}`);
+ }
+ }, promiseCount);
+}
+
+void WasmPromiseTest::simpleReject()
+{
+ init();
+
+ qstdweb::Promise::make(m_testSupport, "makeTestPromise", {
+ .thenFunc = [](val result) {
+ Q_UNUSED(result);
+ QWASMFAIL("Unexpected then");
+ },
+ .catchFunc = [](val result) {
+ QWASMVERIFY(result.isString());
+ QWASMCOMPARE("Evil error", result.as<std::string>());
+ QWASMSUCCESS();
+ }
+ }, std::string("simpleReject"));
+
+ EM_ASM({
+ testSupport.reject["simpleReject"]("Evil error");
+ });
+}
+
+void WasmPromiseTest::multipleReject()
+{
+ static constexpr int promiseCount = 1000;
+
+ auto onCatch = std::make_shared<BarrierCallback>(promiseCount, []() {
+ QWASMSUCCESS();
+ });
+
+ for (int i = 0; i < promiseCount; ++i) {
+ qstdweb::Promise::make(m_testSupport, "makeTestPromise", {
+ .thenFunc = [=](val result) {
+ QWASMVERIFY(result.isString());
+ QWASMCOMPARE(QString::number(i).toStdString(), result.as<std::string>());
+
+ (*onCatch)();
+ },
+ .catchFunc = [](val error) {
+ Q_UNUSED(error);
+ QWASMFAIL("Unexpected catch");
+ }
+ }, (QStringLiteral("test") + QString::number(i)).toStdString());
+ }
+
+ EM_ASM({
+ for (let i = $0 - 1; i >= 0; --i) {
+ testSupport.resolve['test' + i](`${i}`);
+ }
+ }, promiseCount);
+}
+
+void WasmPromiseTest::throwInThen()
+{
+ init();
+
+ qstdweb::Promise::make(m_testSupport, "makeTestPromise", {
+ .thenFunc = [](val result) {
+ Q_UNUSED(result);
+ EM_ASM({
+ throw "Expected error";
+ });
+ },
+ .catchFunc = [](val error) {
+ QWASMCOMPARE("Expected error", error.as<std::string>());
+ QWASMSUCCESS();
+ }
+ }, std::string("throwInThen"));
+
+ EM_ASM({
+ testSupport.resolve["throwInThen"]();
+ });
+}
+
+void WasmPromiseTest::bareFinally()
+{
+ init();
+
+ qstdweb::Promise::make(m_testSupport, "makeTestPromise", {
+ .finallyFunc = []() {
+ QWASMSUCCESS();
+ }
+ }, std::string("bareFinally"));
+
+ EM_ASM({
+ testSupport.resolve["bareFinally"]();
+ });
+}
+
+void WasmPromiseTest::finallyWithThen()
+{
+ init();
+
+ auto thenCalled = std::make_shared<bool>();
+ qstdweb::Promise::make(m_testSupport, "makeTestPromise", {
+ .thenFunc = [thenCalled] (val result) {
+ Q_UNUSED(result);
+ *thenCalled = true;
+ },
+ .finallyFunc = [thenCalled]() {
+ QWASMVERIFY(*thenCalled);
+ QWASMSUCCESS();
+ }
+ }, std::string("finallyWithThen"));
+
+ EM_ASM({
+ testSupport.resolve["finallyWithThen"]();
+ });
+}
+
+void WasmPromiseTest::finallyWithThrow()
+{
+ init();
+
+ qstdweb::Promise::make(m_testSupport, "makeTestPromise", {
+ .catchFunc = [](val error) {
+ Q_UNUSED(error);
+ },
+ .finallyFunc = []() {
+ QWASMSUCCESS();
+ }
+ }, std::string("finallyWithThrow"));
+
+ EM_ASM({
+ testSupport.reject["finallyWithThrow"]();
+ });
+}
+
+void WasmPromiseTest::finallyWithThrowInThen()
+{
+ init();
+
+ qstdweb::Promise::make(m_testSupport, "makeTestPromise", {
+ .thenFunc = [](val result) {
+ Q_UNUSED(result);
+ EM_ASM({
+ throw "Expected error";
+ });
+ },
+ .catchFunc = [](val result) {
+ QWASMVERIFY(result.isString());
+ QWASMCOMPARE("Expected error", result.as<std::string>());
+ },
+ .finallyFunc = []() {
+ QWASMSUCCESS();
+ }
+ }, std::string("bareFinallyWithThen"));
+
+ EM_ASM({
+ testSupport.resolve["bareFinallyWithThen"]();
+ });
+}
+
+void WasmPromiseTest::nested()
+{
+ init();
+
+ qstdweb::Promise::make(m_testSupport, "makeTestPromise", {
+ .thenFunc = [this](val result) {
+ QWASMVERIFY(result.isString());
+ QWASMCOMPARE("Outer data", result.as<std::string>());
+
+ qstdweb::Promise::make(m_testSupport, "makeTestPromise", {
+ .thenFunc = [this](val innerResult) {
+ QWASMVERIFY(innerResult.isString());
+ QWASMCOMPARE("Inner data", innerResult.as<std::string>());
+
+ qstdweb::Promise::make(m_testSupport, "makeTestPromise", {
+ .thenFunc = [](val innerResult) {
+ QWASMVERIFY(innerResult.isString());
+ QWASMCOMPARE("Innermost data", innerResult.as<std::string>());
+
+ QWASMSUCCESS();
+ },
+ .catchFunc = [](val error) {
+ Q_UNUSED(error);
+ QWASMFAIL("Unexpected catch");
+ }
+ }, std::string("innermost"));
+
+ EM_ASM({
+ testSupport.resolve["innermost"]("Innermost data");
+ });
+ },
+ .catchFunc = [](val error) {
+ Q_UNUSED(error);
+ QWASMFAIL("Unexpected catch");
+ }
+ }, std::string("inner"));
+
+ EM_ASM({
+ testSupport.resolve["inner"]("Inner data");
+ });
+ },
+ .catchFunc = [](val error) {
+ Q_UNUSED(error);
+ QWASMFAIL("Unexpected catch");
+ }
+ }, std::string("outer"));
+
+ EM_ASM({
+ testSupport.resolve["outer"]("Outer data");
+ });
+}
+
+void WasmPromiseTest::all()
+{
+ init();
+
+ static constexpr int promiseCount = 1000;
+ auto thenCalledOnce = std::shared_ptr<bool>();
+ *thenCalledOnce = true;
+
+ std::vector<val> promises;
+ promises.reserve(promiseCount);
+
+ for (int i = 0; i < promiseCount; ++i) {
+ promises.push_back(m_testSupport.call<val>("makeTestPromise", val(("all" + QString::number(i)).toStdString())));
+ }
+
+ qstdweb::Promise::all(std::move(promises), {
+ .thenFunc = [=](val result) {
+ QWASMVERIFY(*thenCalledOnce);
+ *thenCalledOnce = false;
+
+ QWASMVERIFY(result.isArray());
+ QWASMCOMPARE(promiseCount, result["length"].as<int>());
+ for (int i = 0; i < promiseCount; ++i) {
+ QWASMCOMPARE(QStringLiteral("Data %1").arg(i).toStdString(), result[i].as<std::string>());
+ }
+
+ QWASMSUCCESS();
+ },
+ .catchFunc = [](val error) {
+ Q_UNUSED(error);
+ QWASMFAIL("Unexpected catch");
+ }
+ });
+
+ EM_ASM({
+ console.log('Resolving');
+ for (let i = $0 - 1; i >= 0; --i) {
+ testSupport.resolve['all' + i](`Data ${i}`);
+ }
+ }, promiseCount);
+}
+
+void WasmPromiseTest::allWithThrow()
+{
+ 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);
+ QWASMFAIL("Unexpected then");
+ },
+ .catchFunc = [catchCalledOnce](val result) {
+ QWASMVERIFY(*catchCalledOnce);
+ *catchCalledOnce = false;
+ QWASMVERIFY(result.isString());
+ QWASMCOMPARE("Error 2", result.as<std::string>());
+ QWASMSUCCESS();
+ }
+ });
+
+ EM_ASM({
+ testSupport.resolve["promise3"]("Data 3");
+ testSupport.resolve["promise1"]("Data 1");
+ testSupport.reject["promise2"]("Error 2");
+ });
+}
+
+void WasmPromiseTest::allWithFinally()
+{
+ 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]() {
+ QWASMVERIFY(*finallyCalledOnce);
+ *finallyCalledOnce = false;
+ QWASMSUCCESS();
+ }
+ });
+
+ EM_ASM({
+ testSupport.resolve["promise3"]("Data 3");
+ testSupport.resolve["promise1"]("Data 1");
+ testSupport.resolve["promise2"]("Data 2");
+ });
+}
+
+void WasmPromiseTest::allWithFinallyAndThrow()
+{
+ 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]() {
+ QWASMVERIFY(*finallyCalledOnce);
+ *finallyCalledOnce = false;
+ QWASMSUCCESS();
+ }
+ });
+
+ EM_ASM({
+ testSupport.resolve["promise3"]("Data 3");
+ testSupport.resolve["promise1"]("Data 1");
+ testSupport.resolve["promise2"]("Data 2");
+ });
+}
+
+int main(int argc, char **argv)
+{
+ auto testObject = std::make_shared<WasmPromiseTest>();
+ QtWasmTest::initTestCase<QCoreApplication>(argc, argv, testObject);
+ return 0;
+}
+
+#include "promise_main.moc"
diff --git a/tests/manual/wasm/qstdweb/qwasmcompositor_auto.html b/tests/manual/wasm/qstdweb/qwasmcompositor_auto.html
new file mode 100644
index 0000000000..f33aab0b9c
--- /dev/null
+++ b/tests/manual/wasm/qstdweb/qwasmcompositor_auto.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<script type="text/javascript" src="qtwasmtestlib.js"></script>
+<script type="text/javascript" src="qwasmcompositor_auto.js"></script>
+<script>
+ window.onload = () => {
+ runTestCase(qwasmcompositor_auto_entry, document.getElementById("log"));
+ };
+</script>
+<p>Running files auto test.</p>
+<div id="log"></div>
diff --git a/tests/manual/wasm/qstdweb/qwasmcompositor_main.cpp b/tests/manual/wasm/qstdweb/qwasmcompositor_main.cpp
new file mode 100644
index 0000000000..e1a9cf604d
--- /dev/null
+++ b/tests/manual/wasm/qstdweb/qwasmcompositor_main.cpp
@@ -0,0 +1,172 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#include <QtCore/QEvent>
+#include <QtCore/QObject>
+#include <QtGui/qwindow.h>
+#include <QtGui/qguiapplication.h>
+#include <QtGui/qoffscreensurface.h>
+#include <QtGui/qpa/qwindowsysteminterface.h>
+#include <QtGui/rhi/qrhi.h>
+
+#include <qtwasmtestlib.h>
+
+#include <emscripten.h>
+#include <emscripten/val.h>
+
+#include <functional>
+#include <memory>
+#include <vector>
+
+namespace tst_qwasmcompositor_internal {
+namespace {
+class Window : public QWindow
+{
+ Q_OBJECT
+public:
+ Window();
+ ~Window() override { qDebug() << "dtor Window"; }
+
+ void keyPressEvent(QKeyEvent *) final;
+
+signals:
+ void exposed();
+ void keyEventReceived();
+ void initFailed();
+
+protected:
+private:
+ void init();
+
+ void exposeEvent(QExposeEvent *) override;
+ bool m_exposedOnce = false;
+
+ std::unique_ptr<QOffscreenSurface> m_fallbackSurface;
+ std::unique_ptr<QRhi> m_rhi;
+};
+
+Window::Window()
+{
+ setSurfaceType(OpenGLSurface);
+}
+
+void Window::exposeEvent(QExposeEvent *)
+{
+ if (isExposed() && !m_exposedOnce) {
+ m_exposedOnce = true;
+ init();
+ emit exposed();
+ }
+}
+
+void Window::keyPressEvent(QKeyEvent *)
+{
+ emit keyEventReceived();
+}
+
+void Window::init()
+{
+ QRhi::Flags rhiFlags = QRhi::EnableDebugMarkers;
+
+ m_fallbackSurface.reset(QRhiGles2InitParams::newFallbackSurface());
+ QRhiGles2InitParams params;
+ params.fallbackSurface = m_fallbackSurface.get();
+ params.window = this;
+
+ // Double init of RHI causes the OpenGL context to be destroyed, which causes a bug with input.
+ m_rhi.reset(QRhi::create(QRhi::OpenGLES2, &params, rhiFlags));
+ m_rhi.reset(QRhi::create(QRhi::OpenGLES2, &params, rhiFlags));
+
+ if (!m_rhi)
+ emit initFailed();
+}
+
+} // namespace
+} // namespace tst_qwasmcompositor_internal
+
+using namespace emscripten;
+
+class QWasmCompositorTest : public QObject
+{
+ Q_OBJECT
+
+public:
+ QWasmCompositorTest() : m_window(val::global("window")), m_testSupport(val::object())
+ {
+ m_window.set("testSupport", m_testSupport);
+ m_testSupport.set("qtSetContainerElements",
+ emscripten::val::module_property("qtSetContainerElements"));
+ }
+
+ ~QWasmCompositorTest() noexcept
+ {
+ qDebug() << this << "In dtor";
+ for (auto it = m_cleanup.rbegin(); it != m_cleanup.rend(); ++it)
+ (*it)();
+ m_window.set("testSupport", emscripten::val::undefined());
+ }
+
+private:
+ void init()
+ {
+ EM_ASM({
+ testSupport.screenElement = document.createElement("div");
+ testSupport.screenElement.id = "test-canvas-qwasmcompositor";
+ document.body.appendChild(testSupport.screenElement);
+ });
+ m_cleanup.emplace_back([]() mutable {
+ EM_ASM({
+ testSupport.qtSetContainerElements([]);
+ testSupport.screenElement.parentElement.removeChild(testSupport.screenElement);
+ });
+ });
+
+ EM_ASM({ testSupport.qtSetContainerElements([testSupport.screenElement]); });
+ }
+
+ template<class T>
+ T *Own(T *plainPtr)
+ {
+ m_cleanup.emplace_back([plainPtr]() mutable { delete plainPtr; });
+ return plainPtr;
+ }
+
+ val m_window;
+ val m_testSupport;
+
+ std::vector<std::function<void()>> m_cleanup;
+
+private slots:
+ void testReceivingKeyboardEventsAfterOpenGLContextReset();
+};
+
+void QWasmCompositorTest::testReceivingKeyboardEventsAfterOpenGLContextReset()
+{
+ init();
+
+ using Window = tst_qwasmcompositor_internal::Window;
+ Window *window = Own(new Window);
+ window->show();
+ window->requestActivate();
+
+ QWindowSystemInterface::flushWindowSystemEvents();
+
+ QObject::connect(window, &Window::keyEventReceived, []() { QWASMSUCCESS(); });
+ QObject::connect(window, &Window::initFailed,
+ []() { QWASMFAIL("Cannot initialize test window"); });
+ QObject::connect(window, &Window::exposed, []() {
+ EM_ASM({
+ testSupport.screenElement.shadowRoot.querySelector('.qt-window')
+ .dispatchEvent(new KeyboardEvent('keydown', { key : 'a' }));
+ });
+ });
+}
+
+int main(int argc, char **argv)
+{
+ auto testObject = std::make_shared<QWasmCompositorTest>();
+ QtWasmTest::initTestCase<QGuiApplication>(argc, argv, testObject);
+ return 0;
+}
+
+#include "qwasmcompositor_main.moc"
diff --git a/tests/manual/wasm/qtloader/tst_qtloader.html b/tests/manual/wasm/qtloader/tst_qtloader.html
new file mode 100644
index 0000000000..c85bccc68d
--- /dev/null
+++ b/tests/manual/wasm/qtloader/tst_qtloader.html
@@ -0,0 +1,19 @@
+<!--
+Copyright (C) 2022 The Qt Company Ltd.
+SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+-->
+
+<!doctype html>
+<html>
+
+<head>
+ <meta charset="utf-8">
+ <title>Qt Loader tests</title>
+ <script type="text/javascript" src="https://sinonjs.org/releases/sinon-14.0.0.js"
+ integrity="sha384-z8J4N1s2hPDn6ClmFXDQkKD/e738VOWcR8JmhztPRa+PgezxQupgZu3LzoBO4Jw8"
+ crossorigin="anonymous"></script>
+ <script src="/src/plugins/platforms/wasm/qtloader.js"></script>
+ <script src="tst_qtloader.js" type="module" defer></script>
+</head>
+<body></body>
+</html>
diff --git a/tests/manual/wasm/qtloader/tst_qtloader.js b/tests/manual/wasm/qtloader/tst_qtloader.js
new file mode 100644
index 0000000000..39e0d12807
--- /dev/null
+++ b/tests/manual/wasm/qtloader/tst_qtloader.js
@@ -0,0 +1,42 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+import { TestRunner } from '../shared/testrunner.js';
+
+class QtLoaderTests
+{
+ async beforeEach() { sinon.stub(window, 'alert'); }
+
+ async afterEach() { sinon.restore(); }
+
+ async sampleTestCase()
+ {
+ await new Promise(resolve =>
+ {
+ window.alert();
+ sinon.assert.calledOnce(window.alert);
+ window.setTimeout(resolve, 4000);
+ });
+ }
+
+ async sampleTestCase2()
+ {
+ await new Promise(resolve =>
+ {
+ window.alert();
+ sinon.assert.calledOnce(window.alert);
+ window.setTimeout(resolve, 1000);
+ });
+ }
+
+ async constructQtLoader()
+ {
+ new QtLoader({});
+ }
+}
+
+(async () =>
+{
+ const runner = new TestRunner(new QtLoaderTests());
+ await runner.runAll();
+})();
diff --git a/tests/manual/wasm/qtloader_integration/CMakeLists.txt b/tests/manual/wasm/qtloader_integration/CMakeLists.txt
new file mode 100644
index 0000000000..2603a05135
--- /dev/null
+++ b/tests/manual/wasm/qtloader_integration/CMakeLists.txt
@@ -0,0 +1,45 @@
+# Copyright (C) 2023 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+
+qt_internal_add_manual_test(tst_qtloader_integration
+ GUI
+ SOURCES
+ main.cpp
+ LIBRARIES
+ Qt::Core
+ Qt::Gui
+ Qt::GuiPrivate
+ Qt::Widgets
+)
+
+set_target_properties(tst_qtloader_integration PROPERTIES QT_WASM_EXTRA_EXPORTED_METHODS "ENV")
+
+add_custom_command(
+ TARGET tst_qtloader_integration POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E copy
+ ${CMAKE_CURRENT_SOURCE_DIR}/tst_qtloader_integration.html
+ ${CMAKE_CURRENT_BINARY_DIR}/tst_qtloader_integration.html)
+
+add_custom_command(
+ TARGET tst_qtloader_integration POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E copy
+ ${CMAKE_CURRENT_SOURCE_DIR}/../../../../src/plugins/platforms/wasm/qtloader.js
+ ${CMAKE_CURRENT_BINARY_DIR}/qtloader.js)
+
+add_custom_command(
+ TARGET tst_qtloader_integration POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E copy
+ ${CMAKE_CURRENT_SOURCE_DIR}/../shared/testrunner.js
+ ${CMAKE_CURRENT_BINARY_DIR}/testrunner.js)
+
+add_custom_command(
+ TARGET tst_qtloader_integration POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E copy
+ ${CMAKE_CURRENT_SOURCE_DIR}/test_body.js
+ ${CMAKE_CURRENT_BINARY_DIR}/test_body.js)
+
+add_custom_command(
+ TARGET tst_qtloader_integration POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E copy
+ ${CMAKE_CURRENT_SOURCE_DIR}/preload.json
+ ${CMAKE_CURRENT_BINARY_DIR}/preload.json)
diff --git a/tests/manual/wasm/qtloader_integration/main.cpp b/tests/manual/wasm/qtloader_integration/main.cpp
new file mode 100644
index 0000000000..4bb502b69c
--- /dev/null
+++ b/tests/manual/wasm/qtloader_integration/main.cpp
@@ -0,0 +1,183 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+#include <QtWidgets/QtWidgets>
+
+#include <iostream>
+#include <sstream>
+
+#include <emscripten/bind.h>
+#include <emscripten/val.h>
+#include <emscripten.h>
+
+#include <QtGui/qpa/qplatformscreen.h>
+
+namespace {
+constexpr int ExitValueImmediateReturn = 42;
+constexpr int ExitValueFromExitApp = 22;
+
+std::string screenInformation()
+{
+ auto screens = qGuiApp->screens();
+ std::ostringstream out;
+ out << "[";
+ const char *separator = "";
+ for (const auto &screen : screens) {
+ out << separator;
+ out << "[" << std::to_string(screen->geometry().x()) << ","
+ << std::to_string(screen->geometry().y()) << ","
+ << std::to_string(screen->geometry().width()) << ","
+ << std::to_string(screen->geometry().height()) << "]";
+ separator = ",";
+ }
+ out << "]";
+ return out.str();
+}
+
+std::string logicalDpi()
+{
+ auto screens = qGuiApp->screens();
+ std::ostringstream out;
+ out << "[";
+ const char *separator = "";
+ for (const auto &screen : screens) {
+ out << separator;
+ out << "[" << std::to_string(screen->handle()->logicalDpi().first) << ", "
+ << std::to_string(screen->handle()->logicalDpi().second) << "]";
+ separator = ",";
+ }
+ out << "]";
+ return out.str();
+}
+
+std::string preloadedFiles()
+{
+ QStringList files = QDir("/preload").entryList(QDir::Files);
+ std::ostringstream out;
+ out << "[";
+ const char *separator = "";
+ for (const auto &file : files) {
+ out << separator;
+ out << file.toStdString();
+ separator = ",";
+ }
+ out << "]";
+ return out.str();
+}
+
+void crash()
+{
+ std::abort();
+}
+
+void stackOverflow()
+{
+ stackOverflow(); // should eventually termniate with exception
+}
+
+void exitApp()
+{
+ emscripten_force_exit(ExitValueFromExitApp);
+}
+
+void produceOutput()
+{
+ fprintf(stdout, "Sample output!\n");
+}
+
+std::string retrieveArguments()
+{
+ auto arguments = QApplication::arguments();
+ std::ostringstream out;
+ out << "[";
+ const char *separator = "";
+ for (const auto &argument : arguments) {
+ out << separator;
+ out << "'" << argument.toStdString() << "'";
+ separator = ",";
+ }
+ out << "]";
+ return out.str();
+}
+
+std::string getEnvironmentVariable(std::string name) {
+ return QString::fromLatin1(qgetenv(name.c_str())).toStdString();
+}
+} // namespace
+
+class AppWindow : public QObject
+{
+ Q_OBJECT
+public:
+ AppWindow() : m_layout(new QVBoxLayout(&m_ui))
+ {
+ addWidget<QLabel>("Qt Loader integration tests");
+
+ m_ui.setLayout(m_layout);
+ }
+
+ void show() { m_ui.show(); }
+
+ ~AppWindow() = default;
+
+private:
+ template<class T, class... Args>
+ T *addWidget(Args... args)
+ {
+ T *widget = new T(std::forward<Args>(args)..., &m_ui);
+ m_layout->addWidget(widget);
+ return widget;
+ }
+
+ QWidget m_ui;
+ QVBoxLayout *m_layout;
+};
+
+int main(int argc, char **argv)
+{
+ QApplication application(argc, argv);
+ const auto arguments = application.arguments();
+ const bool exitImmediately =
+ std::find(arguments.begin(), arguments.end(), QStringLiteral("--exit-immediately"))
+ != arguments.end();
+ if (exitImmediately)
+ emscripten_force_exit(ExitValueImmediateReturn);
+
+ const bool crashImmediately =
+ std::find(arguments.begin(), arguments.end(), QStringLiteral("--crash-immediately"))
+ != arguments.end();
+ if (crashImmediately)
+ crash();
+
+ const bool stackOverflowImmediately =
+ std::find(arguments.begin(), arguments.end(), QStringLiteral("--stack-owerflow-immediately"))
+ != arguments.end();
+ if (stackOverflowImmediately)
+ stackOverflow();
+
+ const bool noGui = std::find(arguments.begin(), arguments.end(), QStringLiteral("--no-gui"))
+ != arguments.end();
+
+ if (!noGui) {
+ AppWindow window;
+ window.show();
+ return application.exec();
+ }
+ return application.exec();
+}
+
+EMSCRIPTEN_BINDINGS(qtLoaderIntegrationTest)
+{
+ emscripten::constant("EXIT_VALUE_IMMEDIATE_RETURN", ExitValueImmediateReturn);
+ emscripten::constant("EXIT_VALUE_FROM_EXIT_APP", ExitValueFromExitApp);
+
+ emscripten::function("screenInformation", &screenInformation);
+ emscripten::function("logicalDpi", &logicalDpi);
+ emscripten::function("preloadedFiles", &preloadedFiles);
+ emscripten::function("crash", &crash);
+ emscripten::function("exitApp", &exitApp);
+ emscripten::function("produceOutput", &produceOutput);
+ emscripten::function("retrieveArguments", &retrieveArguments);
+ emscripten::function("getEnvironmentVariable", &getEnvironmentVariable);
+}
+
+#include "main.moc"
diff --git a/tests/manual/wasm/qtloader_integration/preload.json b/tests/manual/wasm/qtloader_integration/preload.json
new file mode 100644
index 0000000000..d7e09911ff
--- /dev/null
+++ b/tests/manual/wasm/qtloader_integration/preload.json
@@ -0,0 +1,10 @@
+[
+ {
+ "source": "qtloader.js",
+ "destination": "/preload/qtloader.js"
+ },
+ {
+ "source": "$QTDIR/qtlogo.svg",
+ "destination": "/preload/qtlogo.svg"
+ }
+]
diff --git a/tests/manual/wasm/qtloader_integration/test_body.js b/tests/manual/wasm/qtloader_integration/test_body.js
new file mode 100644
index 0000000000..e08ffdefbb
--- /dev/null
+++ b/tests/manual/wasm/qtloader_integration/test_body.js
@@ -0,0 +1,517 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDXLicenseIdentifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+import { Mock, assert, TestRunner } from './testrunner.js';
+
+export class QtLoaderIntegrationTests
+{
+ #testScreenContainers = []
+
+ async beforeEach()
+ {
+ this.#addScreenContainer('screen-container-0', { width: '200px', height: '300px' });
+ }
+
+ async afterEach()
+ {
+ this.#testScreenContainers.forEach(screenContainer =>
+ {
+ document.body.removeChild(screenContainer);
+ });
+ this.#testScreenContainers = [];
+ }
+
+ async missingConfig()
+ {
+ let caughtException;
+ try {
+ await qtLoad();
+ } catch (e) {
+ caughtException = e;
+ }
+
+ assert.isNotUndefined(caughtException);
+ assert.equal('config is required, expected an object', caughtException.message);
+ }
+
+ async missingQtSection()
+ {
+ let caughtException;
+ try {
+ await qtLoad({});
+ } catch (e) {
+ caughtException = e;
+ }
+
+ assert.isNotUndefined(caughtException);
+ assert.equal(
+ 'config.qt is required, expected an object', caughtException.message);
+ }
+
+ async missingEntryFunction()
+ {
+ let caughtException;
+ try {
+ await qtLoad({ qt: {}});
+ } catch (e) {
+ caughtException = e;
+ }
+
+ assert.isNotUndefined(caughtException);
+ assert.equal(
+ 'config.qt.entryFunction is required, expected a function', caughtException.message);
+ }
+
+ async badEntryFunction()
+ {
+ let caughtException;
+ try {
+ await qtLoad({ qt: { entryFunction: 'invalid' }});
+ } catch (e) {
+ caughtException = e;
+ }
+
+ assert.isNotUndefined(caughtException);
+ assert.equal(
+ 'config.qt.entryFunction is required, expected a function', caughtException.message);
+ }
+
+ async environmentVariables()
+ {
+ const instance = await qtLoad({
+ qt: {
+ environment: {
+ variable1: 'value1',
+ variable2: 'value2'
+ },
+ entryFunction: tst_qtloader_integration_entry,
+ containerElements: [this.#testScreenContainers[0]]
+ }
+ });
+ assert.isTrue(instance.getEnvironmentVariable('variable1') === 'value1');
+ assert.isTrue(instance.getEnvironmentVariable('variable2') === 'value2');
+ }
+
+ async screenContainerManipulations()
+ {
+ // ... (do other things), then call addContainerElement() to add a new container/screen.
+ // This can happen either before or after load() is called - loader will route the
+ // call to instance when it's ready.
+ this.#addScreenContainer('appcontainer1', { width: '100px', height: '100px' })
+
+ const instance = await qtLoad({
+ qt: {
+ entryFunction: tst_qtloader_integration_entry,
+ containerElements: this.#testScreenContainers
+ }
+ });
+ {
+ const screenInformation = this.#getScreenInformation(instance);
+
+ assert.equal(2, screenInformation.length);
+ assert.equal(200, screenInformation[0].width);
+ assert.equal(300, screenInformation[0].height);
+ assert.equal(100, screenInformation[1].width);
+ assert.equal(100, screenInformation[1].height);
+ }
+
+ this.#addScreenContainer('appcontainer2', { width: '234px', height: '99px' })
+ instance.qtSetContainerElements(this.#testScreenContainers);
+
+ {
+ const screenInformation = this.#getScreenInformation(instance);
+
+ assert.equal(3, screenInformation.length);
+ assert.equal(200, screenInformation[0].width);
+ assert.equal(300, screenInformation[0].height);
+ assert.equal(100, screenInformation[1].width);
+ assert.equal(100, screenInformation[1].height);
+ assert.equal(234, screenInformation[2].width);
+ assert.equal(99, screenInformation[2].height);
+ }
+
+ document.body.removeChild(this.#testScreenContainers.splice(2, 1)[0]);
+ instance.qtSetContainerElements(this.#testScreenContainers);
+ {
+ const screenInformation = this.#getScreenInformation(instance);
+
+ assert.equal(2, screenInformation.length);
+ assert.equal(200, screenInformation[0].width);
+ assert.equal(300, screenInformation[0].height);
+ assert.equal(100, screenInformation[1].width);
+ assert.equal(100, screenInformation[1].height);
+ }
+ }
+
+ async primaryScreenIsAlwaysFirst()
+ {
+ const instance = await qtLoad({
+ qt: {
+ entryFunction: tst_qtloader_integration_entry,
+ containerElements: this.#testScreenContainers,
+ }
+ });
+ this.#addScreenContainer(
+ 'appcontainer3', { width: '12px', height: '24px' },
+ container => this.#testScreenContainers.splice(0, 0, container));
+ this.#addScreenContainer(
+ 'appcontainer4', { width: '34px', height: '68px' },
+ container => this.#testScreenContainers.splice(1, 0, container));
+
+ instance.qtSetContainerElements(this.#testScreenContainers);
+ {
+ const screenInformation = this.#getScreenInformation(instance);
+
+ assert.equal(3, screenInformation.length);
+ // The primary screen (at position 0) is always at 0
+ assert.equal(12, screenInformation[0].width);
+ assert.equal(24, screenInformation[0].height);
+ // Other screens are pushed at the back
+ assert.equal(200, screenInformation[1].width);
+ assert.equal(300, screenInformation[1].height);
+ assert.equal(34, screenInformation[2].width);
+ assert.equal(68, screenInformation[2].height);
+ }
+
+ this.#testScreenContainers.forEach(screenContainer =>
+ {
+ document.body.removeChild(screenContainer);
+ });
+ this.#testScreenContainers = [
+ this.#addScreenContainer('appcontainer5', { width: '11px', height: '12px' }),
+ this.#addScreenContainer('appcontainer6', { width: '13px', height: '14px' }),
+ ];
+
+ instance.qtSetContainerElements(this.#testScreenContainers);
+ {
+ const screenInformation = this.#getScreenInformation(instance);
+
+ assert.equal(2, screenInformation.length);
+ assert.equal(11, screenInformation[0].width);
+ assert.equal(12, screenInformation[0].height);
+ assert.equal(13, screenInformation[1].width);
+ assert.equal(14, screenInformation[1].height);
+ }
+ }
+
+ async multipleInstances()
+ {
+ // Fetch/Compile the module once; reuse for each instance. This is also if the page wants to
+ // initiate the .wasm file download fetch as early as possible, before the browser has
+ // finished fetching and parsing testapp.js and qtloader.js
+ const module = WebAssembly.compileStreaming(fetch('tst_qtloader_integration.wasm'));
+
+ const instances = await Promise.all([1, 2, 3].map(i => qtLoad({
+ qt: {
+ entryFunction: tst_qtloader_integration_entry,
+ containerElements: [this.#addScreenContainer(`screen-container-${i}`, {
+ width: `${i * 10}px`,
+ height: `${i * 10}px`,
+ })],
+ module,
+ }
+ })));
+ // Confirm the identity of instances by querying their screen widths and heights
+ {
+ const screenInformation = this.#getScreenInformation(instances[0]);
+ console.log();
+ assert.equal(1, screenInformation.length);
+ assert.equal(10, screenInformation[0].width);
+ assert.equal(10, screenInformation[0].height);
+ }
+ {
+ const screenInformation = this.#getScreenInformation(instances[1]);
+ assert.equal(1, screenInformation.length);
+ assert.equal(20, screenInformation[0].width);
+ assert.equal(20, screenInformation[0].height);
+ }
+ {
+ const screenInformation = this.#getScreenInformation(instances[2]);
+ assert.equal(1, screenInformation.length);
+ assert.equal(30, screenInformation[0].width);
+ assert.equal(30, screenInformation[0].height);
+ }
+ }
+
+ async consoleMode()
+ {
+ // 'Console mode' for autotesting type scenarios
+ let accumulatedStdout = '';
+ const instance = await qtLoad({
+ arguments: ['--no-gui'],
+ print: output =>
+ {
+ accumulatedStdout += output;
+ },
+ qt: {
+ entryFunction: tst_qtloader_integration_entry,
+ }
+ });
+
+ this.#callTestInstanceApi(instance, 'produceOutput');
+ assert.equal('Sample output!', accumulatedStdout);
+ }
+
+ async modulePromiseProvided()
+ {
+ await qtLoad({
+ qt: {
+ entryFunction: createQtAppInstance,
+ containerElements: [this.#testScreenContainers[0]],
+ module: WebAssembly.compileStreaming(
+ fetch('tst_qtloader_integration.wasm'))
+ }
+ });
+ }
+
+ async moduleProvided()
+ {
+ await qtLoad({
+ qt: {
+ entryFunction: tst_qtloader_integration_entry,
+ containerElements: [this.#testScreenContainers[0]],
+ module: await WebAssembly.compileStreaming(
+ fetch('tst_qtloader_integration.wasm'))
+ }
+ });
+ }
+
+ async arguments()
+ {
+ const instance = await qtLoad({
+ arguments: ['--no-gui', 'arg1', 'other', 'yetanotherarg'],
+ qt: {
+ entryFunction: tst_qtloader_integration_entry,
+ }
+ });
+ const args = this.#callTestInstanceApi(instance, 'retrieveArguments');
+ assert.equal(5, args.length);
+ assert.isTrue('arg1' === args[2]);
+ assert.equal('other', args[3]);
+ assert.equal('yetanotherarg', args[4]);
+ }
+
+ async moduleProvided_exceptionThrownInFactory()
+ {
+ let caughtException;
+ try {
+ await qtLoad({
+ qt: {
+ entryFunction: tst_qtloader_integration_entry,
+ containerElements: [this.#testScreenContainers[0]],
+ module: Promise.reject(new Error('Failed to load')),
+ }
+ });
+ } catch (e) {
+ caughtException = e;
+ }
+ assert.isTrue(caughtException !== undefined);
+ assert.equal('Failed to load', caughtException.message);
+ }
+
+ async abort()
+ {
+ const onExitMock = new Mock();
+ const instance = await qtLoad({
+ arguments: ['--no-gui'],
+ qt: {
+ onExit: onExitMock,
+ entryFunction: tst_qtloader_integration_entry,
+ }
+ });
+ try {
+ instance.crash();
+ } catch { }
+ assert.equal(1, onExitMock.calls.length);
+ const exitStatus = onExitMock.calls[0][0];
+ assert.isTrue(exitStatus.crashed);
+ assert.isUndefined(exitStatus.code);
+ assert.isNotUndefined(exitStatus.text);
+ }
+
+ async abortImmediately()
+ {
+ const onExitMock = new Mock();
+ let caughtException;
+ try {
+ await qtLoad({
+ arguments: ['--no-gui', '--crash-immediately'],
+ qt: {
+ onExit: onExitMock,
+ entryFunction: tst_qtloader_integration_entry,
+ }
+ });
+ } catch (e) {
+ caughtException = e;
+ }
+
+ assert.isTrue(caughtException !== undefined);
+ assert.equal(1, onExitMock.calls.length);
+ const exitStatus = onExitMock.calls[0][0];
+ assert.isTrue(exitStatus.crashed);
+ assert.isUndefined(exitStatus.code);
+ assert.isNotUndefined(exitStatus.text);
+ }
+
+ async stackOwerflowImmediately()
+ {
+ const onExitMock = new Mock();
+ let caughtException;
+ try {
+ await qtLoad({
+ arguments: ['--no-gui', '--stack-owerflow-immediately'],
+ qt: {
+ onExit: onExitMock,
+ entryFunction: tst_qtloader_integration_entry,
+ }
+ });
+ } catch (e) {
+ caughtException = e;
+ }
+
+ assert.isTrue(caughtException !== undefined);
+ assert.equal(1, onExitMock.calls.length);
+ const exitStatus = onExitMock.calls[0][0];
+ assert.isTrue(exitStatus.crashed);
+ assert.isUndefined(exitStatus.code);
+ // text should be "RangeError: Maximum call stack
+ // size exceeded", or similar.
+ assert.isNotUndefined(exitStatus.text);
+ }
+
+ async userAbortCallbackCalled()
+ {
+ const onAbortMock = new Mock();
+ let instance = await qtLoad({
+ arguments: ['--no-gui'],
+ onAbort: onAbortMock,
+ qt: {
+ entryFunction: tst_qtloader_integration_entry,
+ }
+ });
+ try {
+ instance.crash();
+ } catch (e) {
+ // emscripten throws an 'Aborted' error here, which we ignore for the sake of the test
+ }
+ assert.equal(1, onAbortMock.calls.length);
+ }
+
+ async exit()
+ {
+ const onExitMock = new Mock();
+ let instance = await qtLoad({
+ arguments: ['--no-gui'],
+ qt: {
+ onExit: onExitMock,
+ entryFunction: tst_qtloader_integration_entry,
+ }
+ });
+ // The module is running. onExit should not have been called.
+ assert.equal(0, onExitMock.calls.length);
+ try {
+ instance.exitApp();
+ } catch (e) {
+ // emscripten throws a 'Runtime error: unreachable' error here. We ignore it for the
+ // sake of the test.
+ }
+ assert.equal(1, onExitMock.calls.length);
+ const exitStatus = onExitMock.calls[0][0];
+ assert.isFalse(exitStatus.crashed);
+ assert.equal(instance.EXIT_VALUE_FROM_EXIT_APP, exitStatus.code);
+ assert.isUndefined(exitStatus.text);
+ }
+
+ async exitImmediately()
+ {
+ const onExitMock = new Mock();
+ const instance = await qtLoad({
+ arguments: ['--no-gui', '--exit-immediately'],
+ qt: {
+ onExit: onExitMock,
+ entryFunction: tst_qtloader_integration_entry,
+ }
+ });
+ assert.equal(1, onExitMock.calls.length);
+
+ const exitStatusFromOnExit = onExitMock.calls[0][0];
+
+ assert.isFalse(exitStatusFromOnExit.crashed);
+ assert.equal(instance.EXIT_VALUE_IMMEDIATE_RETURN, exitStatusFromOnExit.code);
+ assert.isUndefined(exitStatusFromOnExit.text);
+ }
+
+ async userQuitCallbackCalled()
+ {
+ const quitMock = new Mock();
+ let instance = await qtLoad({
+ arguments: ['--no-gui'],
+ quit: quitMock,
+ qt: {
+ entryFunction: tst_qtloader_integration_entry,
+ }
+ });
+ try {
+ instance.exitApp();
+ } catch (e) {
+ // emscripten throws a 'Runtime error: unreachable' error here. We ignore it for the
+ // sake of the test.
+ }
+ assert.equal(1, quitMock.calls.length);
+ const [exitCode, exception] = quitMock.calls[0];
+ assert.equal(instance.EXIT_VALUE_FROM_EXIT_APP, exitCode);
+ assert.equal('ExitStatus', exception.name);
+ }
+
+ async preloadFiles()
+ {
+ const instance = await qtLoad({
+ arguments: ["--no-gui"],
+ qt: {
+ preload: ['preload.json'],
+ qtdir: '.',
+ }
+ });
+ const preloadedFiles = instance.preloadedFiles();
+ // Verify that preloaded file list matches files specified in preload.json
+ assert.equal("[qtloader.js,qtlogo.svg]", preloadedFiles);
+ }
+
+ #callTestInstanceApi(instance, apiName)
+ {
+ return eval(instance[apiName]());
+ }
+
+ #getScreenInformation(instance)
+ {
+ return this.#callTestInstanceApi(instance, 'screenInformation').map(elem => ({
+ x: elem[0],
+ y: elem[1],
+ width: elem[2],
+ height: elem[3],
+ }));
+ }
+
+ #addScreenContainer(id, style, inserter)
+ {
+ const container = (() =>
+ {
+ const container = document.createElement('div');
+ container.id = id;
+ container.style.width = style.width;
+ container.style.height = style.height;
+ document.body.appendChild(container);
+ return container;
+ })();
+ inserter ? inserter(container) : this.#testScreenContainers.push(container);
+ return container;
+ }
+}
+
+(async () =>
+{
+ const runner = new TestRunner(new QtLoaderIntegrationTests(), {
+ timeoutSeconds: 10
+ });
+ await runner.runAll();
+})();
diff --git a/tests/manual/wasm/qtloader_integration/tst_qtloader_integration.html b/tests/manual/wasm/qtloader_integration/tst_qtloader_integration.html
new file mode 100644
index 0000000000..7aa7528a1d
--- /dev/null
+++ b/tests/manual/wasm/qtloader_integration/tst_qtloader_integration.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<html lang="en-us">
+
+<head>
+ <title>tst_qtloader_integration</title>
+ <script src='tst_qtloader_integration.js'></script>
+ <script src="qtloader.js" defer></script>
+ <script type="module" src="test_body.js" defer></script>
+</head>
+
+<body></body>
+
+</html>
diff --git a/tests/manual/wasm/qtwasmtestlib/README.md b/tests/manual/wasm/qtwasmtestlib/README.md
new file mode 100644
index 0000000000..6de81fe8b4
--- /dev/null
+++ b/tests/manual/wasm/qtwasmtestlib/README.md
@@ -0,0 +1,75 @@
+QtWasmTestLib - async auto tests for WebAssembly
+================================================
+
+QtWasmTestLib supports auto-test cases in the web browser. Like QTestLib, each
+test case is defined by a QObject subclass with one or more test functions. The
+test functions may be asynchronous, where they return early and then complete
+at some later point.
+
+The test lib is implemented as a C++ and JavaScript library, where the test is written
+using C++ and a hosting html page calls JavaScript API to run the test.
+
+Implementing a basic test case
+------------------------------
+
+In the test cpp file, define the test functions as private slots. All test
+functions must call completeTestFunction() exactly once, or will time out
+otherwise. Subsequent calls to completeTestFunction will be disregarded.
+It is advised to use QWASMSUCCESS/QWASMFAIL for reporting the test execution
+status and QWASMCOMPARE/QWASMVERIFY to assert on test conditions. The call can
+be made after the test function itself has returned.
+
+ class TestTest: public QObject
+ {
+ Q_OBJECT
+ private slots:
+ void timerTest() {
+ QTimer::singleShot(timeout, [](){
+ completeTestFunction();
+ });
+ }
+ };
+
+Then define a main() function which calls initTestCase(). The main()
+function is async too, as per Emscripten default. Build the .cpp file
+as a normal Qt for WebAssembly app.
+
+ int main(int argc, char **argv)
+ {
+ auto testObject = std::make_shared<TestTest>();
+ initTestCase<QCoreApplication>(argc, argv, testObject);
+ return 0;
+ }
+
+Finally provide an html file which hosts the test runner and calls runTestCase()
+
+ <!doctype html>
+ <script type="text/javascript" src="qtwasmtestlib.js"></script>
+ <script type="text/javascript" src="test_case.js"></script>
+ <script>
+ window.onload = async () => {
+ runTestCase(entryFunction, document.getElementById("log"));
+ };
+ </script>
+ <p>Running Foo auto test.</p>
+ <div id="log"></div>
+
+Implementing a GUI test case
+----------------------------
+
+This is similar to implementing a basic test case, with the difference that the hosting
+html file provides container elements which becomes QScreens for the test code.
+
+ <!doctype html>
+ <script type="text/javascript" src="qtwasmtestlib.js"></script>
+ <script type="text/javascript" src="test_case.js"></script>
+ <script>
+ window.onload = async () => {
+ let log = document.getElementById("log")
+ let containers = [document.getElementById("container")];
+ runTestCase(entryFunction, log, containers);
+ };
+ </script>
+ <p>Running Foo auto test.</p>
+ <div id="container"></div>
+ <div id="log"></div>
diff --git a/tests/manual/wasm/qtwasmtestlib/qtwasmtestlib.cpp b/tests/manual/wasm/qtwasmtestlib/qtwasmtestlib.cpp
new file mode 100644
index 0000000000..ec03c7209a
--- /dev/null
+++ b/tests/manual/wasm/qtwasmtestlib/qtwasmtestlib.cpp
@@ -0,0 +1,175 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#include "qtwasmtestlib.h"
+
+#include <QtCore/qmetaobject.h>
+
+#include <emscripten/bind.h>
+#include <emscripten.h>
+#include <emscripten/threading.h>
+
+namespace QtWasmTest {
+namespace {
+QObject *g_testObject = nullptr;
+std::string g_currentTestName;
+std::function<void ()> g_cleanup;
+}
+
+void runOnMainThread(std::function<void(void)> fn);
+static bool isValidSlot(const QMetaMethod &sl);
+
+
+//
+// Public API
+//
+
+// Initializes the test case with a test object and cleanup function. The
+// cleanup function is called when all test functions have completed.
+void initTestCase(QObject *testObject, std::function<void ()> cleanup)
+{
+ g_testObject = testObject;
+ g_cleanup = cleanup;
+}
+
+void verify(bool condition, std::string_view conditionString, std::string_view file, int line)
+{
+ if (!condition) {
+ completeTestFunction(
+ TestResult::Fail,
+ formatMessage(file, line, "Condition failed: " + std::string(conditionString)));
+ }
+}
+
+// Completes the currently running test function with a result. This function is
+// thread-safe and call be called from any thread.
+void completeTestFunction(TestResult result, std::string message)
+{
+ auto resultString = [](TestResult result) {
+ switch (result) {
+ case TestResult::Pass:
+ return "PASS";
+ break;
+ case TestResult::Fail:
+ return "FAIL";
+ break;
+ case TestResult::Skip:
+ return "SKIP";
+ break;
+ }
+ };
+
+ // Report test result to JavaScript test runner, on the main thread
+ runOnMainThread([resultString = resultString(result), message](){
+ EM_ASM({
+ completeTestFunction(UTF8ToString($0), UTF8ToString($1), UTF8ToString($2));
+ }, g_currentTestName.c_str(), resultString, message.c_str());
+ });
+}
+
+// Completes the currently running test function with a Pass result.
+void completeTestFunction()
+{
+ completeTestFunction(TestResult::Pass, std::string());
+}
+
+//
+// Private API for the Javascript test runnner
+//
+
+std::string formatMessage(std::string_view file, int line, std::string_view message)
+{
+ return "[" + std::string(file) + ":" + QString::number(line).toStdString() + "] " + std::string(message);
+}
+
+void cleanupTestCase()
+{
+ g_testObject = nullptr;
+ g_cleanup();
+}
+
+std::string getTestFunctions()
+{
+ std::string testFunctions;
+
+ // Duplicate qPrintTestSlots (private QTestLib function) logic.
+ for (int i = 0; i < g_testObject->metaObject()->methodCount(); ++i) {
+ QMetaMethod sl = g_testObject->metaObject()->method(i);
+ if (!isValidSlot(sl))
+ continue;
+ QByteArray signature = sl.methodSignature();
+ Q_ASSERT(signature.endsWith("()"));
+ signature.chop(2);
+ if (!testFunctions.empty())
+ testFunctions += " ";
+ testFunctions += std::string(signature.constData());
+ }
+
+ return testFunctions;
+}
+
+void runTestFunction(std::string name)
+{
+ g_currentTestName = name;
+ QMetaObject::invokeMethod(g_testObject, name.c_str());
+}
+
+void failTest(std::string message)
+{
+ completeTestFunction(QtWasmTest::Fail, std::move(message));
+}
+
+void passTest()
+{
+ completeTestFunction(QtWasmTest::Pass, "");
+}
+
+EMSCRIPTEN_BINDINGS(qtwebtestrunner) {
+ emscripten::function("cleanupTestCase", &cleanupTestCase);
+ emscripten::function("getTestFunctions", &getTestFunctions);
+ emscripten::function("runTestFunction", &runTestFunction);
+ emscripten::function("qtWasmFail", &failTest);
+ emscripten::function("qtWasmPass", &passTest);
+}
+
+//
+// Test lib implementation
+//
+
+static bool isValidSlot(const QMetaMethod &sl)
+{
+ if (sl.access() != QMetaMethod::Private || sl.parameterCount() != 0
+ || sl.returnType() != QMetaType::Void || sl.methodType() != QMetaMethod::Slot)
+ return false;
+ const QByteArray name = sl.name();
+ return !(name.isEmpty() || name.endsWith("_data")
+ || name == "initTestCase" || name == "cleanupTestCase"
+ || name == "init" || name == "cleanup");
+}
+
+void trampoline(void *context)
+{
+ Q_ASSERT(emscripten_is_main_runtime_thread());
+
+ emscripten_async_call([](void *context) {
+ std::function<void(void)> *fn = reinterpret_cast<std::function<void(void)> *>(context);
+ (*fn)();
+ delete fn;
+ }, context, 0);
+}
+
+// Runs the given function on the main thread, asynchronously
+void runOnMainThread(std::function<void(void)> fn)
+{
+ void *context = new std::function<void(void)>(fn);
+ if (emscripten_is_main_runtime_thread()) {
+ trampoline(context);
+ } else {
+#if QT_CONFIG(thread)
+ emscripten_async_run_in_main_runtime_thread_(EM_FUNC_SIG_VI, reinterpret_cast<void *>(trampoline), context);
+#endif
+ }
+}
+
+} // namespace QtWasmTest
+
diff --git a/tests/manual/wasm/qtwasmtestlib/qtwasmtestlib.h b/tests/manual/wasm/qtwasmtestlib/qtwasmtestlib.h
new file mode 100644
index 0000000000..2307ed1ccd
--- /dev/null
+++ b/tests/manual/wasm/qtwasmtestlib/qtwasmtestlib.h
@@ -0,0 +1,73 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#ifndef QT_WASM_TESTRUNNER_H
+#define QT_WASM_TESTRUNNER_H
+
+#include <QtCore/qobject.h>
+
+#include <functional>
+
+namespace QtWasmTest {
+
+enum TestResult {
+ Pass,
+ Fail,
+ Skip,
+};
+
+std::string formatMessage(std::string_view file,
+ int line,
+ std::string_view message);
+
+void completeTestFunction(TestResult result, std::string message);
+void completeTestFunction();
+void initTestCase(QObject *testObject, std::function<void ()> cleanup);
+template <typename App>
+void initTestCase(int argc,
+ char **argv,
+ std::shared_ptr<QObject> testObject)
+{
+ auto app = std::make_shared<App>(argc, argv);
+ auto cleanup = [testObject, app]() mutable {
+ // C++ lambda capture destruction order is unspecified;
+ // delete test before app by calling reset().
+ testObject.reset();
+ app.reset();
+ };
+ initTestCase(testObject.get(), cleanup);
+}
+void verify(bool condition,
+ std::string_view conditionString,
+ std::string_view file,
+ int line);
+
+template<class L, class R>
+void compare(const L& lhs,
+ const R& rhs,
+ std::string_view lhsString,
+ std::string_view rhsString,
+ std::string_view file,
+ int line) {
+ if (lhs != rhs) {
+ completeTestFunction(
+ TestResult::Fail,
+ formatMessage(file, line, "Comparison failed: " + std::string(lhsString) + " == " + std::string(rhsString)));
+ }
+}
+
+} // namespace QtWasmTest
+
+#define QWASMVERIFY(condition) \
+ QtWasmTest::verify((condition), #condition, __FILE__, __LINE__);
+
+#define QWASMCOMPARE(left, right) \
+ QtWasmTest::compare((left), (right), #left, #right, __FILE__, __LINE__);
+
+#define QWASMSUCCESS() \
+ QtWasmTest::completeTestFunction(QtWasmTest::Pass, "")
+
+#define QWASMFAIL(message) \
+ QtWasmTest::completeTestFunction(QtWasmTest::Fail, QtWasmTest::formatMessage(__FILE__, __LINE__, message))
+
+#endif
diff --git a/tests/manual/wasm/qtwasmtestlib/qtwasmtestlib.js b/tests/manual/wasm/qtwasmtestlib/qtwasmtestlib.js
new file mode 100644
index 0000000000..d4f815b887
--- /dev/null
+++ b/tests/manual/wasm/qtwasmtestlib/qtwasmtestlib.js
@@ -0,0 +1,137 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+// A minimal async test runner for Qt async auto tests.
+//
+// Usage: Call runTest(name, testFunctionCompleted), where "name" is the name of the app
+// (the .wasm file name), and testFunctionCompleted is a test-function-complete
+// callback. The test runner will then instantiate the app and run tests.
+//
+// The test runner expects that the app instance defines the following
+// functions:
+//
+// void cleanupTestCase()
+// string getTestFunctions()
+// runTestFunction(string)
+//
+// Further, the test runner expects that the app instance calls
+// completeTestFunction() (below - note that both the instance and this
+// file have a function with that name) when a test function finishes. This
+// can be done during runTestFunction(), or after it has returned (this
+// is the part which enables async testing). Test functions which fail
+// to call completeTestFunction() will time out after 2000ms.
+//
+const g_maxTime = 2000;
+
+class TestFunction {
+ constructor(instance, name) {
+ this.instance = instance;
+ this.name = name;
+ this.resolve = undefined;
+ this.reject = undefined;
+ this.timeoutId = undefined;
+ }
+
+ complete(result, details) {
+ // Reset timeout
+ clearTimeout(this.timeoutId);
+ this.timeoutId = undefined;
+
+ const callback = result.startsWith('FAIL') ? this.reject : this.resolve;
+ callback(`${result}${details ? ': ' + details : ''}`);
+ }
+
+ run() {
+ // Set timer which will catch test functions
+ // which fail to call completeTestFunction()
+ this.timeoutId = setTimeout(() => {
+ completeTestFunction(this.name, 'FAIL', `Timeout after ${g_maxTime} ms`)
+ }, g_maxTime);
+
+ return new Promise((resolve, reject) => {
+ this.resolve = resolve;
+ this.reject = reject;
+
+ this.instance.runTestFunction(this.name);
+ });
+ }
+};
+
+function completeTestFunction(testFunctionName, result, details) {
+ if (!window.currentTestFunction || testFunctionName !== window.currentTestFunction.name)
+ return;
+
+ window.currentTestFunction.complete(result, details);
+}
+
+async function runTestFunction(instance, name) {
+ if (window.currentTestFunction) {
+ throw new Error(`While trying to run ${name}: Last function hasn't yet finished`);
+ }
+ window.currentTestFunction = new TestFunction(instance, name);
+ try {
+ const result = await window.currentTestFunction.run();
+ return result;
+ } finally {
+ delete window.currentTestFunction;
+ }
+}
+
+async function runTestCaseImpl(entryFunction, testFunctionStarted, testFunctionCompleted, qtContainers)
+{
+ // Create test case instance
+ const config = {
+ qtContainerElements: qtContainers || []
+ }
+ const instance = await entryFunction(config);
+
+ // Run all test functions
+ const functionsString = instance.getTestFunctions();
+ const functions = functionsString.split(" ").filter(Boolean);
+ for (const name of functions) {
+ testFunctionStarted(name);
+ try {
+ const result = await runTestFunction(instance, name);
+ testFunctionCompleted(result);
+ } catch (err) {
+ testFunctionCompleted(err.message ?? err);
+ }
+ }
+
+ // Cleanup
+ instance.cleanupTestCase();
+}
+
+var g_htmlLogElement = undefined;
+
+function testFunctionStarted(name) {
+ let line = name + ": ";
+ g_htmlLogElement.innerHTML += line;
+}
+
+function testFunctionCompleted(status) {
+
+ const color = (status) => {
+ if (status.startsWith("PASS"))
+ return "green";
+ if (status.startsWith("FAIL"))
+ return "red";
+ if (status.startsWith("SKIP"))
+ return "tan";
+ return "black";
+ };
+
+ const line = `<span style='color: ${color(status)};'>${status}</text><br>`;
+ g_htmlLogElement.innerHTML += line;
+}
+
+async function runTestCase(entryFunction, htmlLogElement, qtContainers)
+{
+ g_htmlLogElement = htmlLogElement;
+ try {
+ await runTestCaseImpl(entryFunction, testFunctionStarted, testFunctionCompleted, qtContainers);
+ g_htmlLogElement.innerHTML += "<br> DONE"
+ } catch (err) {
+ g_htmlLogElement.innerHTML += err
+ }
+}
diff --git a/tests/manual/wasm/rasterwindow/CMakeLists.txt b/tests/manual/wasm/rasterwindow/CMakeLists.txt
new file mode 100644
index 0000000000..ed5b7ecd18
--- /dev/null
+++ b/tests/manual/wasm/rasterwindow/CMakeLists.txt
@@ -0,0 +1,11 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+
+qt_internal_add_manual_test(rasterwindow
+ GUI
+ SOURCES
+ main.cpp rasterwindow.cpp
+ LIBRARIES
+ Qt::Core
+ Qt::Gui
+)
diff --git a/tests/manual/wasm/rasterwindow/main.cpp b/tests/manual/wasm/rasterwindow/main.cpp
new file mode 100644
index 0000000000..576b73112b
--- /dev/null
+++ b/tests/manual/wasm/rasterwindow/main.cpp
@@ -0,0 +1,14 @@
+// Copyright (C) 2021 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+#include <QtGui>
+#include "rasterwindow.h"
+
+int main(int argc, char **argv)
+{
+ QGuiApplication app(argc, argv);
+
+ RasterWindow window;
+ window.show();
+
+ return app.exec();
+}
diff --git a/tests/manual/wasm/rasterwindow/rasterwindow.cpp b/tests/manual/wasm/rasterwindow/rasterwindow.cpp
new file mode 100644
index 0000000000..8fd036c274
--- /dev/null
+++ b/tests/manual/wasm/rasterwindow/rasterwindow.cpp
@@ -0,0 +1,185 @@
+// Copyright (C) 2021 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+#include "rasterwindow.h"
+
+RasterWindow::RasterWindow()
+:m_eventCount(0)
+,m_timeoutCount(0)
+,m_frameCount(0)
+,m_fps(0)
+,m_pressed(false)
+{
+ qDebug() << "RasterWindow()";
+
+ // disable alpha; saves filling the window with transparent pixels
+ QSurfaceFormat format;
+ format.setAlphaBufferSize(0);
+ setFormat(format);
+
+ QTimer *timer = new QTimer(this);
+ connect(timer, &QTimer::timeout, [this](){
+ ++m_timeoutCount;
+ m_fps = m_frameCount;
+ m_frameCount = 0;
+ update();
+ });
+ timer->start(1000);
+}
+
+void RasterWindow::paintEvent(QPaintEvent * event)
+{
+ QRect r = event->rect();
+ qDebug() << "RasterWindow::paintEvent" << r;
+
+ ++m_frameCount;
+
+ QPainter p(this);
+
+ QColor fillColor(0, 102, 153);
+ QColor fillColor2(0, 85, 123);
+
+ int tileSize = 40;
+ for (int i = -tileSize * 2; i < r.width() + tileSize * 2; i += tileSize) {
+ for (int j = -tileSize * 2; j < r.height() + tileSize * 2; j += tileSize) {
+ QRect rect(i + (m_offset.x() % tileSize * 2), j + (m_offset.y() % tileSize * 2), tileSize, tileSize);
+ int colorIndex = abs((i/tileSize - j/tileSize) % 2);
+ p.fillRect(rect, colorIndex == 0 ? fillColor : fillColor2);
+ }
+ }
+
+ QRect g = geometry();
+ QRect sg = this->screen()->geometry();
+ QString text;
+ text += QString("Window Geometry: %1 %2 %3 %4\n").arg(g.x()).arg(g.y()).arg(g.width()).arg(g.height());
+ text += QString("Window devicePixelRatio: %1\n").arg(devicePixelRatio());
+ text += QString("Screen Geometry: %1 %2 %3 %4\n").arg(sg.x()).arg(sg.y()).arg(sg.width()).arg(sg.height());
+ text += QString("Received Events: %1\n").arg(m_eventCount);
+ text += QString("Received Timers: %1\n").arg(m_timeoutCount);
+ text += QString("Frames Per Second: %1\n").arg(m_fps);
+
+ p.drawText(QRectF(0, 0, width(), height()), Qt::AlignCenter, text);
+}
+void RasterWindow::exposeEvent(QExposeEvent * ev)
+{
+ qDebug() << __PRETTY_FUNCTION__;
+ QRasterWindow::exposeEvent(ev);
+ incrementEventCount();
+}
+
+void RasterWindow::focusInEvent(QFocusEvent * ev)
+{
+ qDebug() << __PRETTY_FUNCTION__;
+ QRasterWindow::focusInEvent(ev);
+ incrementEventCount();
+}
+
+void RasterWindow::focusOutEvent(QFocusEvent * ev)
+{
+ qDebug() << __PRETTY_FUNCTION__;
+ QRasterWindow::focusOutEvent(ev);
+ incrementEventCount();
+}
+
+void RasterWindow::hideEvent(QHideEvent * ev)
+{
+ qDebug() << __PRETTY_FUNCTION__;
+ QRasterWindow::hideEvent(ev);
+ incrementEventCount();
+}
+
+void RasterWindow::keyPressEvent(QKeyEvent * ev)
+{
+ qDebug() << __PRETTY_FUNCTION__;
+ QRasterWindow::keyPressEvent(ev);
+ incrementEventCount();
+}
+
+void RasterWindow::keyReleaseEvent(QKeyEvent * ev)
+{
+ qDebug() << __PRETTY_FUNCTION__;
+ QRasterWindow::keyReleaseEvent(ev);
+ incrementEventCount();
+}
+
+void RasterWindow::mouseDoubleClickEvent(QMouseEvent * ev)
+{
+ qDebug() << __PRETTY_FUNCTION__;
+ QRasterWindow::mouseDoubleClickEvent(ev);
+ incrementEventCount();
+}
+
+void RasterWindow::mouseMoveEvent(QMouseEvent * ev)
+{
+ qDebug() << __PRETTY_FUNCTION__;
+ QRasterWindow::mouseMoveEvent(ev);
+ incrementEventCount();
+
+ if (m_pressed)
+ m_offset += ev->position().toPoint() - m_lastPos;
+ m_lastPos = ev->position().toPoint();
+}
+
+void RasterWindow::mousePressEvent(QMouseEvent * ev)
+{
+ qDebug() << __PRETTY_FUNCTION__;
+ QRasterWindow::mousePressEvent(ev);
+ incrementEventCount();
+ m_pressed = true;
+}
+
+void RasterWindow::mouseReleaseEvent(QMouseEvent * ev)
+{
+ qDebug() << __PRETTY_FUNCTION__;
+ QRasterWindow::mouseReleaseEvent(ev);
+ incrementEventCount();
+ m_pressed = false;
+}
+
+void RasterWindow::moveEvent(QMoveEvent * ev)
+{
+ qDebug() << __PRETTY_FUNCTION__;
+ QRasterWindow::moveEvent(ev);
+ incrementEventCount();
+}
+
+void RasterWindow::resizeEvent(QResizeEvent * ev)
+{
+ qDebug() << __PRETTY_FUNCTION__;
+ QRasterWindow::resizeEvent(ev);
+ incrementEventCount();
+}
+
+void RasterWindow::showEvent(QShowEvent * ev)
+{
+ qDebug() << __PRETTY_FUNCTION__;
+ QRasterWindow::showEvent(ev);
+ incrementEventCount();
+}
+
+void RasterWindow::tabletEvent(QTabletEvent * ev)
+{
+ qDebug() << __PRETTY_FUNCTION__;
+ QRasterWindow::tabletEvent(ev);
+ incrementEventCount();
+}
+
+void RasterWindow::touchEvent(QTouchEvent * ev)
+{
+ qDebug() << __PRETTY_FUNCTION__;
+ QRasterWindow::touchEvent(ev);
+ incrementEventCount();
+}
+
+void RasterWindow::wheelEvent(QWheelEvent * ev)
+{
+ qDebug() << __PRETTY_FUNCTION__;
+ QRasterWindow::wheelEvent(ev);
+ incrementEventCount();
+ m_offset += ev->pixelDelta();
+}
+
+void RasterWindow::incrementEventCount()
+{
+ ++m_eventCount;
+ update();
+}
diff --git a/tests/manual/wasm/rasterwindow/rasterwindow.h b/tests/manual/wasm/rasterwindow/rasterwindow.h
new file mode 100644
index 0000000000..e488420440
--- /dev/null
+++ b/tests/manual/wasm/rasterwindow/rasterwindow.h
@@ -0,0 +1,43 @@
+// Copyright (C) 2021 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+#ifndef RASTERWINDOW_H
+#define RASTERWINDOW_H
+
+#include <QtGui>
+
+class RasterWindow : public QRasterWindow
+{
+public:
+ RasterWindow();
+
+ virtual void paintEvent(QPaintEvent * event);
+
+ virtual void exposeEvent(QExposeEvent * ev);
+ virtual void focusInEvent(QFocusEvent * ev);
+ virtual void focusOutEvent(QFocusEvent * ev);
+ virtual void hideEvent(QHideEvent * ev);
+ virtual void keyPressEvent(QKeyEvent * ev);
+ virtual void keyReleaseEvent(QKeyEvent * ev);
+ virtual void mouseDoubleClickEvent(QMouseEvent * ev);
+ virtual void mouseMoveEvent(QMouseEvent * ev);
+ virtual void mousePressEvent(QMouseEvent * ev);
+ virtual void mouseReleaseEvent(QMouseEvent * ev);
+ virtual void moveEvent(QMoveEvent * ev);
+// virtual bool nativeEvent(const QByteArray & eventType, void * message, long * result);
+ virtual void resizeEvent(QResizeEvent * ev);
+ virtual void showEvent(QShowEvent * ev);
+ virtual void tabletEvent(QTabletEvent * ev);
+ virtual void touchEvent(QTouchEvent * ev);
+ virtual void wheelEvent(QWheelEvent * ev);
+private:
+ void incrementEventCount();
+ int m_eventCount;
+ int m_timeoutCount;
+ int m_frameCount;
+ int m_fps;
+ QPoint m_offset;
+ QPoint m_lastPos;
+ bool m_pressed;
+};
+
+#endif // RASTERWINDOW_H
diff --git a/tests/manual/wasm/shared/.gitignore b/tests/manual/wasm/shared/.gitignore
new file mode 100644
index 0000000000..ba077a4031
--- /dev/null
+++ b/tests/manual/wasm/shared/.gitignore
@@ -0,0 +1 @@
+bin
diff --git a/tests/manual/wasm/shared/run.sh b/tests/manual/wasm/shared/run.sh
new file mode 100755
index 0000000000..f04e45278c
--- /dev/null
+++ b/tests/manual/wasm/shared/run.sh
@@ -0,0 +1,30 @@
+#! /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()
+{
+ kill $cleanupPid
+}
+
+if [ -z "$1"]
+then
+ echo "Usage: $0 testname, where testname is a test in the tests/manual/wasm directory" >&2
+ exit 1
+fi
+
+trap removeServer EXIT
+
+script_dir=`dirname ${BASH_SOURCE[0]}`
+cd "$script_dir/../../../../"
+python3 util/wasm/qtwasmserver/qtwasmserver.py -p 8001 &
+cleanupPid=$!
+cd -
+
+python3 -m webbrowser "http://localhost:8001/tests/manual/wasm/$1/tst_$1.html"
+
+echo 'Press any key to continue...' >&2
+read -n 1
diff --git a/tests/manual/wasm/shared/testrunner.js b/tests/manual/wasm/shared/testrunner.js
new file mode 100644
index 0000000000..197e3bfa6d
--- /dev/null
+++ b/tests/manual/wasm/shared/testrunner.js
@@ -0,0 +1,161 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+function parseQuery()
+{
+ const trimmed = window.location.search.substring(1);
+ return new Map(
+ trimmed.length === 0 ?
+ [] :
+ trimmed.split('&').map(paramNameAndValue =>
+ {
+ const [name, value] = paramNameAndValue.split('=');
+ return [decodeURIComponent(name), value ? decodeURIComponent(value) : ''];
+ }));
+}
+
+export class assert
+{
+ static isFalse(value)
+ {
+ if (value !== false)
+ throw new Error(`Assertion failed, expected to be false, was ${value}`);
+ }
+
+ static isTrue(value)
+ {
+ if (value !== true)
+ throw new Error(`Assertion failed, expected to be true, was ${value}`);
+ }
+
+ static isUndefined(value)
+ {
+ if (typeof value !== 'undefined')
+ throw new Error(`Assertion failed, expected to be undefined, was ${value}`);
+ }
+
+ static isNotUndefined(value)
+ {
+ if (typeof value === 'undefined')
+ throw new Error(`Assertion failed, expected not to be undefined, was ${value}`);
+ }
+
+ static equal(expected, actual)
+ {
+ if (expected !== actual)
+ throw new Error(`Assertion failed, expected to be ${expected}, was ${actual}`);
+ }
+
+ static notEqual(expected, actual)
+ {
+ if (expected === actual)
+ throw new Error(`Assertion failed, expected not to be ${expected}`);
+ }
+}
+
+export class Mock extends Function
+{
+ #calls = [];
+
+ constructor()
+ {
+ super()
+ const proxy = new Proxy(this, {
+ apply: (target, _, args) => target.onCall(...args)
+ });
+ proxy.thisMock = this;
+
+ return proxy;
+ }
+
+ get calls()
+ {
+ return this.thisMock.#calls;
+ }
+
+ onCall(...args)
+ {
+ this.#calls.push(args);
+ }
+}
+
+function output(message)
+{
+ const outputLine = document.createElement('div');
+ outputLine.style.fontFamily = 'monospace';
+ outputLine.innerText = message;
+
+ document.body.appendChild(outputLine);
+
+ console.log(message);
+}
+
+export class TestRunner
+{
+ #testClassInstance
+ #timeoutSeconds
+
+ constructor(testClassInstance, config)
+ {
+ this.#testClassInstance = testClassInstance;
+ this.#timeoutSeconds = config?.timeoutSeconds ?? 2;
+ }
+
+ async run(testCase)
+ {
+ const prototype = Object.getPrototypeOf(this.#testClassInstance);
+ try {
+ output(`Running ${testCase}`);
+ if (!prototype.hasOwnProperty(testCase))
+ throw new Error(`No such testcase ${testCase}`);
+
+ if (prototype.beforeEach) {
+ await prototype.beforeEach.apply(this.#testClassInstance);
+ }
+
+ await new Promise((resolve, reject) =>
+ {
+ let rejected = false;
+ const timeout = window.setTimeout(() =>
+ {
+ rejected = true;
+ reject(new Error(`Timeout after ${this.#timeoutSeconds} seconds`));
+ }, this.#timeoutSeconds * 1000);
+ prototype[testCase].apply(this.#testClassInstance).then(() =>
+ {
+ if (!rejected) {
+ window.clearTimeout(timeout);
+ output(`✅ Test passed ${testCase}`);
+ resolve();
+ }
+ }).catch(reject);
+ });
+ } catch (e) {
+ output(`❌ Failed ${testCase}: exception ${e} ${e.stack}`);
+ } finally {
+ if (prototype.afterEach) {
+ await prototype.afterEach.apply(this.#testClassInstance);
+ }
+ }
+ }
+
+ async runAll()
+ {
+ const query = parseQuery();
+ const testFilter = query.has('testfilter') ? new RegExp(query.get('testfilter')) : undefined;
+
+ const SPECIAL_FUNCTIONS =
+ ['beforeEach', 'afterEach', 'beforeAll', 'afterAll', 'constructor'];
+ const prototype = Object.getPrototypeOf(this.#testClassInstance);
+ const testFunctions =
+ Object.getOwnPropertyNames(prototype).filter(
+ entry => SPECIAL_FUNCTIONS.indexOf(entry) === -1 && (!testFilter || entry.match(testFilter)));
+
+ if (prototype.beforeAll)
+ await prototype.beforeAll.apply(this.#testClassInstance);
+ for (const fn of testFunctions)
+ await this.run(fn);
+ if (prototype.afterAll)
+ await prototype.afterAll.apply(this.#testClassInstance);
+ }
+}