summaryrefslogtreecommitdiffstats
path: root/tests/manual/wasm
diff options
context:
space:
mode:
Diffstat (limited to 'tests/manual/wasm')
-rw-r--r--tests/manual/wasm/CMakeLists.txt6
-rw-r--r--tests/manual/wasm/README.md1
-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.txt18
-rw-r--r--tests/manual/wasm/clipboard/clipboard.pro2
-rw-r--r--tests/manual/wasm/clipboard/main.cpp2
-rw-r--r--tests/manual/wasm/clipboard/mainwindow.cpp35
-rw-r--r--tests/manual/wasm/clipboard/mainwindow.h2
-rw-r--r--tests/manual/wasm/cursors/CMakeLists.txt5
-rw-r--r--tests/manual/wasm/cursors/MainWindow.cpp2
-rw-r--r--tests/manual/wasm/cursors/MainWindow.h2
-rw-r--r--tests/manual/wasm/cursors/main.cpp2
-rw-r--r--tests/manual/wasm/eventloop/CMakeLists.txt4
-rw-r--r--tests/manual/wasm/eventloop/README.md1
-rw-r--r--tests/manual/wasm/eventloop/asyncify_exec/CMakeLists.txt8
-rw-r--r--tests/manual/wasm/eventloop/asyncify_exec/main.cpp12
-rw-r--r--tests/manual/wasm/eventloop/dialog_exec/CMakeLists.txt5
-rw-r--r--tests/manual/wasm/eventloop/dialog_exec/main.cpp2
-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.txt5
-rw-r--r--tests/manual/wasm/eventloop/main_exec/main.cpp2
-rw-r--r--tests/manual/wasm/eventloop/main_noexec/CMakeLists.txt5
-rw-r--r--tests/manual/wasm/eventloop/main_noexec/main.cpp2
-rw-r--r--tests/manual/wasm/eventloop/thread_exec/CMakeLists.txt5
-rw-r--r--tests/manual/wasm/eventloop/thread_exec/main.cpp2
-rw-r--r--tests/manual/wasm/localfiles/CMakeLists.txt5
-rw-r--r--tests/manual/wasm/localfiles/main.cpp179
-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.txt5
-rw-r--r--tests/manual/wasm/rasterwindow/main.cpp2
-rw-r--r--tests/manual/wasm/rasterwindow/rasterwindow.cpp2
-rw-r--r--tests/manual/wasm/rasterwindow/rasterwindow.h2
-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
78 files changed, 4618 insertions, 105 deletions
diff --git a/tests/manual/wasm/CMakeLists.txt b/tests/manual/wasm/CMakeLists.txt
index 4bd7341d66..b13f6781b8 100644
--- a/tests/manual/wasm/CMakeLists.txt
+++ b/tests/manual/wasm/CMakeLists.txt
@@ -1,7 +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
index 5117e2f70b..9266f38cc6 100644
--- a/tests/manual/wasm/README.md
+++ b/tests/manual/wasm/README.md
@@ -12,3 +12,4 @@ Content
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
index 4bc60a5edc..40fb8ca308 100644
--- a/tests/manual/wasm/clipboard/CMakeLists.txt
+++ b/tests/manual/wasm/clipboard/CMakeLists.txt
@@ -1,4 +1,5 @@
-# Generated from clipboard.pro.
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
#####################################################################
## clipboard Binary:
@@ -9,7 +10,9 @@ qt_internal_add_manual_test(clipboard
SOURCES
main.cpp
mainwindow.cpp mainwindow.h mainwindow.ui
- PUBLIC_LIBRARIES
+ NO_PCH_SOURCES
+ main.cpp # undef QT_NO_FOREACH
+ LIBRARIES
Qt::Core
Qt::Gui
Qt::Widgets
@@ -32,15 +35,6 @@ qt_internal_add_resource(clipboard "data"
#####################################################################
qt_internal_extend_target(clipboard CONDITION (QT_MAJOR_VERSION GREATER 4)
- PUBLIC_LIBRARIES
+ LIBRARIES
Qt::Widgets
)
-
-#### Keys ignored in scope 3:.:.:clipboard.pro:QNX:
-# target.path = "/tmp/$${TARGET}/bin"
-
-#### Keys ignored in scope 5:.:.:clipboard.pro:UNIX AND NOT ANDROID:
-# target.path = "/opt/$${TARGET}/bin"
-
-#### Keys ignored in scope 6:.:.:clipboard.pro:NOT target.path_ISEMPTY:
-# INSTALLS = "target"
diff --git a/tests/manual/wasm/clipboard/clipboard.pro b/tests/manual/wasm/clipboard/clipboard.pro
index 3286049225..cffce46997 100644
--- a/tests/manual/wasm/clipboard/clipboard.pro
+++ b/tests/manual/wasm/clipboard/clipboard.pro
@@ -6,7 +6,7 @@ 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_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
+#DEFINES += QT_DISABLE_DEPRECATED_UP_TO=0x060000 # disables all APIs deprecated in Qt 6.0.0 and earlier
SOURCES += \
main.cpp \
diff --git a/tests/manual/wasm/clipboard/main.cpp b/tests/manual/wasm/clipboard/main.cpp
index 7142125ff7..aa838f6670 100644
--- a/tests/manual/wasm/clipboard/main.cpp
+++ b/tests/manual/wasm/clipboard/main.cpp
@@ -1,5 +1,5 @@
// Copyright (C) 2021 The Qt Company Ltd.
-// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include "mainwindow.h"
diff --git a/tests/manual/wasm/clipboard/mainwindow.cpp b/tests/manual/wasm/clipboard/mainwindow.cpp
index 77d6582775..81a95c4218 100644
--- a/tests/manual/wasm/clipboard/mainwindow.cpp
+++ b/tests/manual/wasm/clipboard/mainwindow.cpp
@@ -1,5 +1,7 @@
// Copyright (C) 2021 The Qt Company Ltd.
-// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+// 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"
@@ -12,6 +14,7 @@
#include <QKeyEvent>
#include <QMimeDatabase>
#include <QFileInfo>
+#include <QCryptographicHash>
#ifdef Q_OS_WASM
#include <emscripten.h>
@@ -262,20 +265,19 @@ void MainWindow::dropEvent(QDropEvent* e)
QString urlStr = url.toDisplayString();
int size = urlStr.length();
sizeStr.setNum(size);
- ui->textEdit_2->insertPlainText(" Drop has url data length: " + sizeStr + "\n");
- ui->textEdit_2->insertPlainText(urlStr + "\n");
-
- QString fname = url.toLocalFile();
- QFileInfo info(fname);
- if (info.exists()) { // this is a file
- QMimeDatabase db;
- QMimeType mt = db.mimeTypeForFile(info);
- if (mt.name().contains("image")) {
- QImage image = QImage(fname);
- setImage(image);
- }
+
+ 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();
@@ -292,14 +294,15 @@ void MainWindow::dropEvent(QDropEvent* e)
int size = e->mimeData()->html().length();
sizeStr.setNum(size);
ui->textEdit_2->insertPlainText(" Drop has html data length: " + sizeStr + "\n");
- ui->textEdit_2->insertPlainText(e->mimeData()->html()+"\n");
- ui->textEdit->insertHtml(e->mimeData()->html()+"<br>");
+ 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");
- ui->textEdit_2->insertPlainText(e->mimeData()->text());
+ for (const auto &line : e->mimeData()->text().split('\n', Qt::SkipEmptyParts))
+ ui->textEdit_2->insertPlainText(" " + line + "\n");
}
const QString message = tr(" Drop accepted, %1 ")
diff --git a/tests/manual/wasm/clipboard/mainwindow.h b/tests/manual/wasm/clipboard/mainwindow.h
index d06b213971..fe101ad494 100644
--- a/tests/manual/wasm/clipboard/mainwindow.h
+++ b/tests/manual/wasm/clipboard/mainwindow.h
@@ -1,5 +1,5 @@
// Copyright (C) 2021 The Qt Company Ltd.
-// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
diff --git a/tests/manual/wasm/cursors/CMakeLists.txt b/tests/manual/wasm/cursors/CMakeLists.txt
index 674c6d4d2b..93f93064cf 100644
--- a/tests/manual/wasm/cursors/CMakeLists.txt
+++ b/tests/manual/wasm/cursors/CMakeLists.txt
@@ -1,9 +1,12 @@
+# 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
- PUBLIC_LIBRARIES
+ LIBRARIES
Qt::Core
Qt::Gui
Qt::Widgets
diff --git a/tests/manual/wasm/cursors/MainWindow.cpp b/tests/manual/wasm/cursors/MainWindow.cpp
index b62c6752aa..c6e4fbcca1 100644
--- a/tests/manual/wasm/cursors/MainWindow.cpp
+++ b/tests/manual/wasm/cursors/MainWindow.cpp
@@ -1,5 +1,5 @@
// Copyright (C) 2019 The Qt Company Ltd.
-// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+// 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 ed570a72c4..ebaeed9e5c 100644
--- a/tests/manual/wasm/cursors/MainWindow.h
+++ b/tests/manual/wasm/cursors/MainWindow.h
@@ -1,5 +1,5 @@
// Copyright (C) 2019 The Qt Company Ltd.
-// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+// 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 99a1d41524..9a59cdd994 100644
--- a/tests/manual/wasm/cursors/main.cpp
+++ b/tests/manual/wasm/cursors/main.cpp
@@ -1,5 +1,5 @@
// Copyright (C) 2019 The Qt Company Ltd.
-// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+// 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
index 96c7fd45bb..132fd15dbb 100644
--- a/tests/manual/wasm/eventloop/CMakeLists.txt
+++ b/tests/manual/wasm/eventloop/CMakeLists.txt
@@ -1,4 +1,8 @@
+# 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)
diff --git a/tests/manual/wasm/eventloop/README.md b/tests/manual/wasm/eventloop/README.md
index e5d4b92306..e1a5a1a3b7 100644
--- a/tests/manual/wasm/eventloop/README.md
+++ b/tests/manual/wasm/eventloop/README.md
@@ -12,3 +12,4 @@ Contents
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
index 09b5cdb1e9..fe7cfb9030 100644
--- a/tests/manual/wasm/eventloop/asyncify_exec/CMakeLists.txt
+++ b/tests/manual/wasm/eventloop/asyncify_exec/CMakeLists.txt
@@ -1,6 +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
- PUBLIC_LIBRARIES
+ 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
index c3a827ac11..f09163184d 100644
--- a/tests/manual/wasm/eventloop/asyncify_exec/main.cpp
+++ b/tests/manual/wasm/eventloop/asyncify_exec/main.cpp
@@ -1,15 +1,13 @@
// Copyright (C) 2021 The Qt Company Ltd.
-// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <QtCore>
-// This test shows how to asyncify enables blocking
-// the main thread on QEventLoop::exec(), while event
-// provessing continues.
+// 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);
-#ifdef QT_HAVE_EMSCRIPTEN_ASYNCIFY
QTimer::singleShot(1000, []() {
QEventLoop loop;
@@ -22,10 +20,6 @@ int main(int argc, char **argv)
loop.exec();
qDebug() << "Returned from QEventLoop::exec()";
});
-#else
- qDebug() << "This test requires Emscripten asyncify. To enable,"
- << "configure Qt with -device-option QT_EMSCRIPTEN_ASYNCIFY=1";
-#endif
app.exec();
}
diff --git a/tests/manual/wasm/eventloop/dialog_exec/CMakeLists.txt b/tests/manual/wasm/eventloop/dialog_exec/CMakeLists.txt
index 00bbca0b9d..ac18643c63 100644
--- a/tests/manual/wasm/eventloop/dialog_exec/CMakeLists.txt
+++ b/tests/manual/wasm/eventloop/dialog_exec/CMakeLists.txt
@@ -1,8 +1,11 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+
qt_internal_add_manual_test(dialog_exec
GUI
SOURCES
main.cpp
- PUBLIC_LIBRARIES
+ 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
index adf8a02c37..f5b072fc0b 100644
--- a/tests/manual/wasm/eventloop/dialog_exec/main.cpp
+++ b/tests/manual/wasm/eventloop/dialog_exec/main.cpp
@@ -1,5 +1,5 @@
// Copyright (C) 2021 The Qt Company Ltd.
-// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <QtGui>
#include <QtWidgets>
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
index b5d987f8cb..1f263ddbcf 100644
--- a/tests/manual/wasm/eventloop/main_exec/CMakeLists.txt
+++ b/tests/manual/wasm/eventloop/main_exec/CMakeLists.txt
@@ -1,8 +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
- PUBLIC_LIBRARIES
+ 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
index c981fd4c2c..17eccafe18 100644
--- a/tests/manual/wasm/eventloop/main_exec/main.cpp
+++ b/tests/manual/wasm/eventloop/main_exec/main.cpp
@@ -1,5 +1,5 @@
// Copyright (C) 2021 The Qt Company Ltd.
-// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <QtGui>
// This example demonstrates how the standard Qt main()
diff --git a/tests/manual/wasm/eventloop/main_noexec/CMakeLists.txt b/tests/manual/wasm/eventloop/main_noexec/CMakeLists.txt
index 23d48b626f..e929089479 100644
--- a/tests/manual/wasm/eventloop/main_noexec/CMakeLists.txt
+++ b/tests/manual/wasm/eventloop/main_noexec/CMakeLists.txt
@@ -1,8 +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
- PUBLIC_LIBRARIES
+ 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
index 18d0542137..6ddd88bd14 100644
--- a/tests/manual/wasm/eventloop/main_noexec/main.cpp
+++ b/tests/manual/wasm/eventloop/main_noexec/main.cpp
@@ -1,5 +1,5 @@
// Copyright (C) 2021 The Qt Company Ltd.
-// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <QtGui>
// This example demonstrates how to create QGuiApplication
diff --git a/tests/manual/wasm/eventloop/thread_exec/CMakeLists.txt b/tests/manual/wasm/eventloop/thread_exec/CMakeLists.txt
index 1c845510df..765ccee4f1 100644
--- a/tests/manual/wasm/eventloop/thread_exec/CMakeLists.txt
+++ b/tests/manual/wasm/eventloop/thread_exec/CMakeLists.txt
@@ -1,8 +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
- PUBLIC_LIBRARIES
+ 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
index b24be17e5b..589066b34d 100644
--- a/tests/manual/wasm/eventloop/thread_exec/main.cpp
+++ b/tests/manual/wasm/eventloop/thread_exec/main.cpp
@@ -1,5 +1,5 @@
// Copyright (C) 2021 The Qt Company Ltd.
-// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <QtGui>
class EventTarget : public QObject
diff --git a/tests/manual/wasm/localfiles/CMakeLists.txt b/tests/manual/wasm/localfiles/CMakeLists.txt
index 4e06a916ad..6d607d1f5a 100644
--- a/tests/manual/wasm/localfiles/CMakeLists.txt
+++ b/tests/manual/wasm/localfiles/CMakeLists.txt
@@ -1,8 +1,11 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+
qt_internal_add_manual_test(localfiles
GUI
SOURCES
main.cpp
- PUBLIC_LIBRARIES
+ LIBRARIES
Qt::Core
Qt::Gui
Qt::Widgets
diff --git a/tests/manual/wasm/localfiles/main.cpp b/tests/manual/wasm/localfiles/main.cpp
index 46e2b058c6..862bff50a4 100644
--- a/tests/manual/wasm/localfiles/main.cpp
+++ b/tests/manual/wasm/localfiles/main.cpp
@@ -1,53 +1,138 @@
// Copyright (C) 2019 The Qt Company Ltd.
-// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+// 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
index cd3a2eef3d..ed5b7ecd18 100644
--- a/tests/manual/wasm/rasterwindow/CMakeLists.txt
+++ b/tests/manual/wasm/rasterwindow/CMakeLists.txt
@@ -1,8 +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
- PUBLIC_LIBRARIES
+ LIBRARIES
Qt::Core
Qt::Gui
)
diff --git a/tests/manual/wasm/rasterwindow/main.cpp b/tests/manual/wasm/rasterwindow/main.cpp
index 38921c8c30..576b73112b 100644
--- a/tests/manual/wasm/rasterwindow/main.cpp
+++ b/tests/manual/wasm/rasterwindow/main.cpp
@@ -1,5 +1,5 @@
// Copyright (C) 2021 The Qt Company Ltd.
-// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <QtGui>
#include "rasterwindow.h"
diff --git a/tests/manual/wasm/rasterwindow/rasterwindow.cpp b/tests/manual/wasm/rasterwindow/rasterwindow.cpp
index b8da476d46..8fd036c274 100644
--- a/tests/manual/wasm/rasterwindow/rasterwindow.cpp
+++ b/tests/manual/wasm/rasterwindow/rasterwindow.cpp
@@ -1,5 +1,5 @@
// Copyright (C) 2021 The Qt Company Ltd.
-// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include "rasterwindow.h"
RasterWindow::RasterWindow()
diff --git a/tests/manual/wasm/rasterwindow/rasterwindow.h b/tests/manual/wasm/rasterwindow/rasterwindow.h
index b01efb9ddb..e488420440 100644
--- a/tests/manual/wasm/rasterwindow/rasterwindow.h
+++ b/tests/manual/wasm/rasterwindow/rasterwindow.h
@@ -1,5 +1,5 @@
// Copyright (C) 2021 The Qt Company Ltd.
-// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#ifndef RASTERWINDOW_H
#define 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);
+ }
+}