diff options
Diffstat (limited to 'tests/manual/wasm')
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, ¶ms, rhiFlags)); + m_rhi.reset(QRhi::create(QRhi::OpenGLES2, ¶ms, 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); + } +} |