diff options
author | Mikolaj Boc <mikolaj.boc@qt.io> | 2022-06-21 17:50:04 +0200 |
---|---|---|
committer | Mikolaj Boc <mikolaj.boc@qt.io> | 2022-06-30 01:20:28 +0200 |
commit | d0eba2449adcee6f1daa33426f9c9b8ebf7c28f5 (patch) | |
tree | c651a3f7759a0a46f3f792a79697d91e374994f7 | |
parent | 97665c9615ff399e9a074b94926ab06e0c9619e5 (diff) |
Create a promise wrapper for C++ and port existing uses
Currently, to use a promise from C++ we either have to use an ASM block
(which does not work well with dynamic linking) or declare exports in
the EMSCRIPTEN_BINDINGS block, which is cumbersome and cannot be chained.
This solution makes it easy to use js promises by introducing the
WebPromiseManager which dispatches callbacks to appropriate callers when
available.
This is a preliminary patch for FileSystem support, which will heavily
use async APIs.
Task-number: QTBUG-99611
Change-Id: I368a8f173027eaa883a9ca18d0ea6a3e99b86071
Reviewed-by: Morten Johan Sørvig <morten.sorvig@qt.io>
-rw-r--r-- | src/corelib/CMakeLists.txt | 15 | ||||
-rw-r--r-- | src/corelib/platform/wasm/qstdweb.cpp | 242 | ||||
-rw-r--r-- | src/corelib/platform/wasm/qstdweb_p.h | 32 | ||||
-rw-r--r-- | src/corelib/platform/wasm/qtcontextfulpromise_injection.js | 32 | ||||
-rw-r--r-- | src/plugins/platforms/wasm/qwasmclipboard.cpp | 109 | ||||
-rw-r--r-- | tests/auto/wasm/CMakeLists.txt | 17 | ||||
-rw-r--r-- | tests/auto/wasm/tst_qstdweb.cpp | 629 |
7 files changed, 977 insertions, 99 deletions
diff --git a/src/corelib/CMakeLists.txt b/src/corelib/CMakeLists.txt index f89aaae573..5394ee642d 100644 --- a/src/corelib/CMakeLists.txt +++ b/src/corelib/CMakeLists.txt @@ -1359,3 +1359,18 @@ if(APPLE AND QT_FEATURE_framework AND QT_FEATURE_separate_debug_info) DESTINATION "${dsym_script_install_dir}" ) endif() + +if(WASM) + set(wasm_injections + "${CMAKE_CURRENT_SOURCE_DIR}/platform/wasm/qtcontextfulpromise_injection.js" + ) + + qt_internal_add_resource(Core "wasminjections" + PREFIX + "/injections" + BASE + "${CMAKE_CURRENT_SOURCE_DIR}/platform/wasm" + FILES + ${wasm_injections} + ) +endif() diff --git a/src/corelib/platform/wasm/qstdweb.cpp b/src/corelib/platform/wasm/qstdweb.cpp index a915c031fe..d13c374dc4 100644 --- a/src/corelib/platform/wasm/qstdweb.cpp +++ b/src/corelib/platform/wasm/qstdweb.cpp @@ -3,7 +3,10 @@ #include "qstdweb_p.h" +#include <QtCore/qcoreapplication.h> +#include <QtCore/qfile.h> #include <emscripten/bind.h> +#include <emscripten/emscripten.h> #include <cstdint> #include <iostream> @@ -11,7 +14,187 @@ QT_BEGIN_NAMESPACE namespace qstdweb { +const char makeContextfulPromiseFunctionName[] = "makePromise"; + typedef double uint53_t; // see Number.MAX_SAFE_INTEGER +namespace { +enum class CallbackType { + Then, + Catch, + Finally, +}; + +void validateCallbacks(const PromiseCallbacks& callbacks) { + Q_ASSERT(!!callbacks.catchFunc || !!callbacks.finallyFunc || !!callbacks.thenFunc); +} + +void injectScript(const std::string& source, const std::string& injectionName) +{ + using namespace emscripten; + + auto script = val::global("document").call<val>("createElement", val("script")); + auto head = val::global("document").call<val>("getElementsByTagName", val("head")); + + script.call<void>("setAttribute", val("qtinjection"), val(injectionName)); + script.set("innerText", val(source)); + + head[0].call<void>("appendChild", std::move(script)); +} + +using PromiseContext = int; + +class WebPromiseManager +{ +public: + static const char contextfulPromiseSupportObjectName[]; + + static const char webPromiseManagerCallbackThunkExportName[]; + + WebPromiseManager(); + ~WebPromiseManager(); + + WebPromiseManager(const WebPromiseManager& other) = delete; + WebPromiseManager(WebPromiseManager&& other) = delete; + WebPromiseManager& operator=(const WebPromiseManager& other) = delete; + WebPromiseManager& operator=(WebPromiseManager&& other) = delete; + + void adoptPromise(emscripten::val target, PromiseCallbacks callbacks); + + static WebPromiseManager* get(); + + static void callbackThunk(emscripten::val callbackType, emscripten::val context, emscripten::val result); + +private: + static std::optional<CallbackType> parseCallbackType(emscripten::val callbackType); + + void subscribeToJsPromiseCallbacks(const PromiseCallbacks& callbacks, emscripten::val jsContextfulPromise); + void callback(CallbackType type, emscripten::val context, emscripten::val result); + + void registerPromise(PromiseContext context, PromiseCallbacks promise); + void unregisterPromise(PromiseContext context); + + QHash<PromiseContext, PromiseCallbacks> m_promiseRegistry; + int m_nextContextId = 0; +}; + +static void qStdWebCleanup() +{ + auto window = emscripten::val::global("window"); + auto contextfulPromiseSupport = window[WebPromiseManager::contextfulPromiseSupportObjectName]; + if (contextfulPromiseSupport.isUndefined()) + return; + + contextfulPromiseSupport.call<void>("removeRef"); +} + +const char WebPromiseManager::webPromiseManagerCallbackThunkExportName[] = "qtStdWebWebPromiseManagerCallbackThunk"; +const char WebPromiseManager::contextfulPromiseSupportObjectName[] = "qtContextfulPromiseSupport"; + +Q_GLOBAL_STATIC(WebPromiseManager, webPromiseManager) + +WebPromiseManager::WebPromiseManager() +{ + QFile injection(QStringLiteral(":/injections/qtcontextfulpromise_injection.js")); + if (!injection.open(QIODevice::ReadOnly)) + qFatal("Missing resource"); + injectScript(injection.readAll().toStdString(), "contextfulpromise"); + qAddPostRoutine(&qStdWebCleanup); +} + +std::optional<CallbackType> +WebPromiseManager::parseCallbackType(emscripten::val callbackType) +{ + if (!callbackType.isString()) + return std::nullopt; + + const std::string data = callbackType.as<std::string>(); + if (data == "then") + return CallbackType::Then; + if (data == "catch") + return CallbackType::Catch; + if (data == "finally") + return CallbackType::Finally; + return std::nullopt; +} + +WebPromiseManager::~WebPromiseManager() = default; + +WebPromiseManager *WebPromiseManager::get() +{ + return webPromiseManager(); +} + +void WebPromiseManager::callbackThunk(emscripten::val callbackType, + emscripten::val context, + emscripten::val result) +{ + auto parsedCallbackType = parseCallbackType(callbackType); + if (!parsedCallbackType) { + qFatal("Bad callback type"); + } + WebPromiseManager::get()->callback(*parsedCallbackType, context, std::move(result)); +} + +void WebPromiseManager::subscribeToJsPromiseCallbacks(const PromiseCallbacks& callbacks, emscripten::val jsContextfulPromiseObject) { + using namespace emscripten; + + if (Q_LIKELY(callbacks.thenFunc)) + jsContextfulPromiseObject = jsContextfulPromiseObject.call<val>("then"); + if (callbacks.catchFunc) + jsContextfulPromiseObject = jsContextfulPromiseObject.call<val>("catch"); + if (callbacks.finallyFunc) + jsContextfulPromiseObject = jsContextfulPromiseObject.call<val>("finally"); +} + +void WebPromiseManager::callback(CallbackType type, emscripten::val context, emscripten::val result) +{ + auto found = m_promiseRegistry.find(context.as<PromiseContext>()); + if (found == m_promiseRegistry.end()) { + return; + } + + bool expectingOtherCallbacks; + switch (type) { + case CallbackType::Then: + found->thenFunc(result); + // At this point, if there is no finally function, we are sure that the Catch callback won't be issued. + expectingOtherCallbacks = !!found->finallyFunc; + break; + case CallbackType::Catch: + found->catchFunc(result); + expectingOtherCallbacks = !!found->finallyFunc; + break; + case CallbackType::Finally: + found->finallyFunc(); + expectingOtherCallbacks = false; + break; + } + + if (!expectingOtherCallbacks) + unregisterPromise(context.as<int>()); +} + +void WebPromiseManager::registerPromise(PromiseContext context, PromiseCallbacks callbacks) +{ + m_promiseRegistry.emplace(context, std::move(callbacks)); +} + +void WebPromiseManager::unregisterPromise(PromiseContext context) +{ + m_promiseRegistry.remove(context); +} + +void WebPromiseManager::adoptPromise(emscripten::val target, PromiseCallbacks callbacks) { + emscripten::val context(m_nextContextId++); + + auto jsContextfulPromise = emscripten::val::global("window") + [contextfulPromiseSupportObjectName].call<emscripten::val>( + makeContextfulPromiseFunctionName, target, context, + emscripten::val::module_property(webPromiseManagerCallbackThunkExportName)); + subscribeToJsPromiseCallbacks(callbacks, jsContextfulPromise); + registerPromise(context.as<int>(), std::move(callbacks)); +} +} // namespace ArrayBuffer::ArrayBuffer(uint32_t size) { @@ -167,6 +350,10 @@ File FileList::operator[](int index) const return item(index); } +emscripten::val FileList::val() { + return m_fileList; +} + ArrayBuffer FileReader::result() const { return ArrayBuffer(m_fileReader["result"]); @@ -322,6 +509,61 @@ std::string EventCallback::contextPropertyName(const std::string &eventName) EMSCRIPTEN_BINDINGS(qtStdwebCalback) { emscripten::function("qtStdWebEventCallbackActivate", &EventCallback::activate); + emscripten::function(WebPromiseManager::webPromiseManagerCallbackThunkExportName, &WebPromiseManager::callbackThunk); +} + +namespace Promise { + void adoptPromise(emscripten::val promiseObject, PromiseCallbacks callbacks) { + validateCallbacks(callbacks); + + WebPromiseManager::get()->adoptPromise( + std::move(promiseObject), std::move(callbacks)); + } + + void all(std::vector<emscripten::val> promises, PromiseCallbacks callbacks) { + struct State { + std::map<int, emscripten::val> results; + int remainingThenCallbacks; + int remainingFinallyCallbacks; + }; + + validateCallbacks(callbacks); + + auto state = std::make_shared<State>(); + state->remainingThenCallbacks = state->remainingFinallyCallbacks = promises.size(); + + for (size_t i = 0; i < promises.size(); ++i) { + PromiseCallbacks individualPromiseCallback; + if (callbacks.thenFunc) { + individualPromiseCallback.thenFunc = [i, state, callbacks](emscripten::val partialResult) mutable { + state->results.emplace(i, std::move(partialResult)); + if (!--(state->remainingThenCallbacks)) { + std::vector<emscripten::val> transformed; + for (auto& data : state->results) { + transformed.push_back(std::move(data.second)); + } + callbacks.thenFunc(emscripten::val::array(std::move(transformed))); + } + }; + } + if (callbacks.catchFunc) { + individualPromiseCallback.catchFunc = [state, callbacks](emscripten::val error) mutable { + callbacks.catchFunc(error); + }; + } + individualPromiseCallback.finallyFunc = [state, callbacks]() mutable { + if (!--(state->remainingFinallyCallbacks)) { + if (callbacks.finallyFunc) + callbacks.finallyFunc(); + // Explicitly reset here for verbosity, this would have been done automatically with the + // destruction of the adopted promise in WebPromiseManager. + state.reset(); + } + }; + + adoptPromise(std::move(promises.at(i)), std::move(individualPromiseCallback)); + } + } } } // namespace qstdweb diff --git a/src/corelib/platform/wasm/qstdweb_p.h b/src/corelib/platform/wasm/qstdweb_p.h index dc8f643507..b4b8948b3a 100644 --- a/src/corelib/platform/wasm/qstdweb_p.h +++ b/src/corelib/platform/wasm/qstdweb_p.h @@ -19,12 +19,14 @@ #include <emscripten/val.h> #include <cstdint> #include <functional> +#include "initializer_list" #include <QtCore/qglobal.h> - +#include "QtCore/qhash.h" QT_BEGIN_NAMESPACE namespace qstdweb { + extern const char makeContextfulPromiseFunctionName[]; // DOM API in C++, implemented using emscripten val.h and bind.h. // This is private API and can be extended and changed as needed. @@ -153,6 +155,34 @@ namespace qstdweb { std::string m_eventName; std::function<void(emscripten::val)> m_fn; }; + + struct PromiseCallbacks + { + std::function<void(emscripten::val)> thenFunc; + std::function<void(emscripten::val)> catchFunc; + std::function<void()> finallyFunc; + }; + + namespace Promise { + void adoptPromise(emscripten::val promise, PromiseCallbacks callbacks); + + template<typename... Args> + void make(emscripten::val target, + QString methodName, + PromiseCallbacks callbacks, + Args... args) + { + emscripten::val promiseObject = target.call<emscripten::val>( + methodName.toStdString().c_str(), std::forward<Args>(args)...); + if (promiseObject.isUndefined() || promiseObject["constructor"]["name"].as<std::string>() != "Promise") { + qFatal("This function did not return a promise"); + } + + adoptPromise(std::move(promiseObject), std::move(callbacks)); + } + + void all(std::vector<emscripten::val> promises, PromiseCallbacks callbacks); + }; } QT_END_NAMESPACE diff --git a/src/corelib/platform/wasm/qtcontextfulpromise_injection.js b/src/corelib/platform/wasm/qtcontextfulpromise_injection.js new file mode 100644 index 0000000000..ce5623171c --- /dev/null +++ b/src/corelib/platform/wasm/qtcontextfulpromise_injection.js @@ -0,0 +1,32 @@ +`Copyright (C) 2022 The Qt Company Ltd. +Copyright (C) 2016 Intel Corporation. +SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0`; + +if (window.qtContextfulPromiseSupport) { + ++window.qtContextfulPromiseSupport.refs; +} else { + window.qtContextfulPromiseSupport = { + refs: 1, + removeRef: () => { + --window.qtContextfulPromiseSupport.refs, 0 === window.qtContextfulPromiseSupport.refs && delete window.qtContextfulPromiseSupport; + }, + makePromise: (a, b, c) => new window.qtContextfulPromiseSupport.ContextfulPromise(a, b, c), + }; + + window.qtContextfulPromiseSupport.ContextfulPromise = class { + constructor(a, b, c) { + (this.wrappedPromise = a), (this.context = b), (this.callbackThunk = c); + } + then() { + return (this.wrappedPromise = this.wrappedPromise.then((a) => { this.callbackThunk("then", this.context, a); })), this; + } + catch() { + return (this.wrappedPromise = this.wrappedPromise.catch((a) => { this.callbackThunk("catch", this.context, a); })), this; + } + finally() { + return (this.wrappedPromise = this.wrappedPromise.finally(() => this.callbackThunk("finally", this.context, undefined))), this; + } + }; +} + +document.querySelector("[qtinjection=contextfulpromise]")?.remove(); diff --git a/src/plugins/platforms/wasm/qwasmclipboard.cpp b/src/plugins/platforms/wasm/qwasmclipboard.cpp index 88043ba092..99f3e61155 100644 --- a/src/plugins/platforms/wasm/qwasmclipboard.cpp +++ b/src/plugins/platforms/wasm/qwasmclipboard.cpp @@ -19,83 +19,6 @@ QT_BEGIN_NAMESPACE using namespace emscripten; -static void pasteClipboardData(emscripten::val format, emscripten::val dataPtr) -{ - QString formatString = QWasmString::toQString(format); - QByteArray dataArray = QByteArray::fromStdString(dataPtr.as<std::string>()); - - QMimeData *mMimeData = new QMimeData; - mMimeData->setData(formatString, dataArray); - - QWasmClipboard::qWasmClipboardPaste(mMimeData); -// QWasmIntegration::get()->getWasmClipboard()->isPaste = false; -} - -static void qClipboardPasteResolve(emscripten::val blob) -{ - // read Blob here - - auto fileReader = std::make_shared<qstdweb::FileReader>(); - auto _blob = qstdweb::Blob(blob); - QString formatString = QString::fromStdString(_blob.type()); - - fileReader->readAsArrayBuffer(_blob); - char *chunkBuffer = nullptr; - qstdweb::ArrayBuffer result = fileReader->result(); - qstdweb::Uint8Array(result).copyTo(chunkBuffer); - QMimeData *mMimeData = new QMimeData; - mMimeData->setData(formatString, chunkBuffer); - QWasmClipboard::qWasmClipboardPaste(mMimeData); -} - -static void qClipboardPromiseResolve(emscripten::val clipboardItems) -{ - int itemsCount = clipboardItems["length"].as<int>(); - - for (int i = 0; i < itemsCount; i++) { - int typesCount = clipboardItems[i]["types"]["length"].as<int>(); // ClipboardItem - - std::string mimeFormat = clipboardItems[i]["types"][0].as<std::string>(); - - if (mimeFormat.find(std::string("text")) != std::string::npos) { - // simple val object, no further processing - - val navigator = val::global("navigator"); - val textPromise = navigator["clipboard"].call<val>("readText"); - val readTextResolve = val::global("Module")["qtClipboardTextPromiseResolve"]; - textPromise.call<val>("then", readTextResolve); - - } else { - // binary types require additional processing - for (int j = 0; j < typesCount; j++) { - val pasteResolve = emscripten::val::module_property("qtClipboardPasteResolve"); - val pasteException = emscripten::val::module_property("qtClipboardPromiseException"); - - // get the blob - clipboardItems[i] - .call<val>("getType", clipboardItems[i]["types"][j]) - .call<val>("then", pasteResolve) - .call<val>("catch", pasteException); - } - } - } -} - -static void qClipboardCopyPromiseResolve(emscripten::val something) -{ - Q_UNUSED(something) - qWarning() << "copy succeeeded"; -} - - -static emscripten::val qClipboardPromiseException(emscripten::val something) -{ - qWarning() << "clipboard error" - << QString::fromStdString(something["name"].as<std::string>()) - << QString::fromStdString(something["message"].as<std::string>()); - return something; -} - static void commonCopyEvent(val event) { QMimeData *_mimes = QWasmIntegration::get()->getWasmClipboard()->mimeData(QClipboard::Clipboard); @@ -215,24 +138,10 @@ static void qClipboardPasteTo(val dataTransfer) QWasmIntegration::get()->getWasmClipboard()->m_isListener = false; } -static void qClipboardTextPromiseResolve(emscripten::val clipdata) -{ - pasteClipboardData(emscripten::val("text/plain"), clipdata); -} - EMSCRIPTEN_BINDINGS(qtClipboardModule) { - function("qtPasteClipboardData", &pasteClipboardData); - - function("qtClipboardTextPromiseResolve", &qClipboardTextPromiseResolve); - function("qtClipboardPromiseResolve", &qClipboardPromiseResolve); - - function("qtClipboardCopyPromiseResolve", &qClipboardCopyPromiseResolve); - function("qtClipboardPromiseException", &qClipboardPromiseException); - function("qtClipboardCutTo", &qClipboardCutTo); function("qtClipboardCopyTo", &qClipboardCopyTo); function("qtClipboardPasteTo", &qClipboardPasteTo); - function("qtClipboardPasteResolve", &qClipboardPasteResolve); } QWasmClipboard::QWasmClipboard() : @@ -419,14 +328,18 @@ void QWasmClipboard::writeToClipboardApi() // break; } - val copyResolve = emscripten::val::module_property("qtClipboardCopyPromiseResolve"); - val copyException = emscripten::val::module_property("qtClipboardPromiseException"); - val navigator = val::global("navigator"); - navigator["clipboard"] - .call<val>("write", clipboardWriteArray) - .call<val>("then", copyResolve) - .call<val>("catch", copyException); + + qstdweb::Promise::make( + navigator["clipboard"], "write", + { + .catchFunc = [](emscripten::val error) { + qWarning() << "clipboard error" + << QString::fromStdString(error["name"].as<std::string>()) + << QString::fromStdString(error["message"].as<std::string>()); + } + }, + clipboardWriteArray); } void QWasmClipboard::writeToClipboard(const QMimeData *data) diff --git a/tests/auto/wasm/CMakeLists.txt b/tests/auto/wasm/CMakeLists.txt new file mode 100644 index 0000000000..2e6c6976fb --- /dev/null +++ b/tests/auto/wasm/CMakeLists.txt @@ -0,0 +1,17 @@ +##################################################################### +## tst_wasm Test: +##################################################################### + +qt_internal_add_test(tst_qstdweb + SOURCES + tst_qstdweb.cpp + DEFINES + QT_NO_FOREACH + QT_NO_KEYWORDS + LIBRARIES + Qt::GuiPrivate + PUBLIC_LIBRARIES + Qt::Core + Qt::Gui + Qt::Widgets +) diff --git a/tests/auto/wasm/tst_qstdweb.cpp b/tests/auto/wasm/tst_qstdweb.cpp new file mode 100644 index 0000000000..f074866a3d --- /dev/null +++ b/tests/auto/wasm/tst_qstdweb.cpp @@ -0,0 +1,629 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// Copyright (C) 2016 Intel Corporation. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include <QtCore/qtimer.h> +#include <QtCore/private/qstdweb_p.h> +#include <QTest> +#include <emscripten.h> +#include <emscripten/bind.h> +#include <emscripten/val.h> + +#if defined(QT_HAVE_EMSCRIPTEN_ASYNCIFY) +#define SKIP_IF_NO_ASYNCIFY() +#else +#define SKIP_IF_NO_ASYNCIFY() QSKIP("Needs QT_HAVE_EMSCRIPTEN_ASYNCIFY") +#endif + +using namespace emscripten; + +class tst_QStdWeb : public QObject +{ + Q_OBJECT +public: + tst_QStdWeb() : m_window(val::global("window")), m_testSupport(val::object()) { + instance = this; + + m_window.set("testSupport", m_testSupport); + } + ~tst_QStdWeb() noexcept {} + +private: + static tst_QStdWeb* instance; + + void init() { + EM_ASM({ + testSupport.resolve = {}; + testSupport.reject = {}; + testSupport.promises = {}; + testSupport.waitConditionPromise = new Promise((resolve, reject) => { + testSupport.finishWaiting = resolve; + }); + + testSupport.makeTestPromise = (param) => { + testSupport.promises[param] = new Promise((resolve, reject) => { + testSupport.resolve[param] = resolve; + testSupport.reject[param] = reject; + }); + + return testSupport.promises[param]; + }; + }); + } + + val m_window; + val m_testSupport; + +private Q_SLOTS: + void simpleResolve(); + void multipleResolve(); + void simpleReject(); + void multipleReject(); + void throwInThen(); + void bareFinally(); + void finallyWithThen(); + void finallyWithThrow(); + void finallyWithThrowInThen(); + void nested(); + void all(); + void allWithThrow(); + void allWithFinally(); + void allWithFinallyAndThrow(); +}; + +tst_QStdWeb* tst_QStdWeb::instance = nullptr; + +EM_ASYNC_JS(void, awaitCondition, (), { + await testSupport.waitConditionPromise; +}); + +void tst_QStdWeb::simpleResolve() +{ + SKIP_IF_NO_ASYNCIFY(); + + init(); + + qstdweb::Promise::make(m_testSupport, "makeTestPromise", { + .thenFunc = [](val result) { + QVERIFY(result.isString()); + QCOMPARE("Some lovely data", result.as<std::string>()); + EM_ASM({ + testSupport.finishWaiting(); + }); + }, + .catchFunc = [](val error) { + Q_UNUSED(error); + QFAIL("Unexpected catch"); + } + }, std::string("simpleResolve")); + + EM_ASM({ + testSupport.resolve["simpleResolve"]("Some lovely data"); + }); + + awaitCondition(); +} + +void tst_QStdWeb::multipleResolve() +{ + SKIP_IF_NO_ASYNCIFY(); + + init(); + + qstdweb::Promise::make(m_testSupport, "makeTestPromise", { + .thenFunc = [](val result) { + QVERIFY(result.isString()); + QCOMPARE("Data 1", result.as<std::string>()); + + EM_ASM({ + if (!--testSupport.promisesLeft) { + testSupport.finishWaiting(); + } + }); + }, + .catchFunc = [](val error) { + Q_UNUSED(error); + QFAIL("Unexpected catch"); + } + }, std::string("1")); + qstdweb::Promise::make(m_testSupport, "makeTestPromise", { + .thenFunc = [](val result) { + QVERIFY(result.isString()); + QCOMPARE("Data 2", result.as<std::string>()); + + EM_ASM({ + if (!--testSupport.promisesLeft) { + testSupport.finishWaiting(); + } + }); + }, + .catchFunc = [](val error) { + Q_UNUSED(error); + QFAIL("Unexpected catch"); + } + }, std::string("2")); + qstdweb::Promise::make(m_testSupport, "makeTestPromise", { + .thenFunc = [](val result) { + QVERIFY(result.isString()); + QCOMPARE("Data 3", result.as<std::string>()); + + EM_ASM({ + if (!--testSupport.promisesLeft) { + testSupport.finishWaiting(); + } + }); + }, + .catchFunc = [](val error) { + Q_UNUSED(error); + QFAIL("Unexpected catch"); + } + }, std::string("3")); + + EM_ASM({ + testSupport.resolve["3"]("Data 3"); + testSupport.resolve["1"]("Data 1"); + testSupport.resolve["2"]("Data 2"); + }); + + awaitCondition(); +} + +void tst_QStdWeb::simpleReject() +{ + SKIP_IF_NO_ASYNCIFY(); + + init(); + + qstdweb::Promise::make(m_testSupport, "makeTestPromise", { + .thenFunc = [](val result) { + Q_UNUSED(result); + QFAIL("Unexpected then"); + }, + .catchFunc = [](val result) { + QVERIFY(result.isString()); + QCOMPARE("Evil error", result.as<std::string>()); + EM_ASM({ + testSupport.finishWaiting(); + }); + } + }, std::string("simpleReject")); + + EM_ASM({ + testSupport.reject["simpleReject"]("Evil error"); + }); + + awaitCondition(); +} + +void tst_QStdWeb::multipleReject() +{ + SKIP_IF_NO_ASYNCIFY(); + + init(); + + qstdweb::Promise::make(m_testSupport, "makeTestPromise", { + .thenFunc = [](val result) { + Q_UNUSED(result); + QFAIL("Unexpected then"); + }, + .catchFunc = [](val error) { + QVERIFY(error.isString()); + QCOMPARE("Error 1", error.as<std::string>()); + + EM_ASM({ + if (!--testSupport.promisesLeft) { + testSupport.finishWaiting(); + } + }); + } + }, std::string("1")); + qstdweb::Promise::make(m_testSupport, "makeTestPromise", { + .thenFunc = [](val result) { + Q_UNUSED(result); + QFAIL("Unexpected then"); + }, + .catchFunc = [](val error) { + QVERIFY(error.isString()); + QCOMPARE("Error 2", error.as<std::string>()); + + EM_ASM({ + if (!--testSupport.promisesLeft) { + testSupport.finishWaiting(); + } + }); + } + }, std::string("2")); + qstdweb::Promise::make(m_testSupport, "makeTestPromise", { + .thenFunc = [](val result) { + Q_UNUSED(result); + QFAIL("Unexpected then"); + }, + .catchFunc = [](val error) { + QVERIFY(error.isString()); + QCOMPARE("Error 3", error.as<std::string>()); + + EM_ASM({ + if (!--testSupport.promisesLeft) { + testSupport.finishWaiting(); + } + }); + } + }, std::string("3")); + + EM_ASM({ + testSupport.reject["3"]("Error 3"); + testSupport.reject["1"]("Error 1"); + testSupport.reject["2"]("Error 2"); + }); + + awaitCondition(); +} + +void tst_QStdWeb::throwInThen() +{ + SKIP_IF_NO_ASYNCIFY(); + + init(); + + qstdweb::Promise::make(m_testSupport, "makeTestPromise", { + .thenFunc = [](val result) { + Q_UNUSED(result); + EM_ASM({ + throw "Expected error"; + }); + }, + .catchFunc = [](val error) { + QCOMPARE("Expected error", error.as<std::string>()); + EM_ASM({ + testSupport.finishWaiting(); + }); + } + }, std::string("throwInThen")); + + EM_ASM({ + testSupport.resolve["throwInThen"](); + }); + + awaitCondition(); +} + +void tst_QStdWeb::bareFinally() +{ + SKIP_IF_NO_ASYNCIFY(); + + init(); + + qstdweb::Promise::make(m_testSupport, "makeTestPromise", { + .finallyFunc = []() { + EM_ASM({ + testSupport.finishWaiting(); + }); + } + }, std::string("bareFinally")); + + EM_ASM({ + testSupport.resolve["bareFinally"](); + }); + + awaitCondition(); +} + +void tst_QStdWeb::finallyWithThen() +{ + SKIP_IF_NO_ASYNCIFY(); + + init(); + + qstdweb::Promise::make(m_testSupport, "makeTestPromise", { + .thenFunc = [] (val result) { + Q_UNUSED(result); + }, + .finallyFunc = []() { + EM_ASM({ + testSupport.finishWaiting(); + }); + } + }, std::string("finallyWithThen")); + + EM_ASM({ + testSupport.resolve["finallyWithThen"](); + }); + + awaitCondition(); +} + +void tst_QStdWeb::finallyWithThrow() +{ + SKIP_IF_NO_ASYNCIFY(); + + init(); + + qstdweb::Promise::make(m_testSupport, "makeTestPromise", { + .catchFunc = [](val error) { + Q_UNUSED(error); + }, + .finallyFunc = []() { + EM_ASM({ + testSupport.finishWaiting(); + }); + } + }, std::string("finallyWithThrow")); + + EM_ASM({ + testSupport.reject["finallyWithThrow"](); + }); + + awaitCondition(); +} + +void tst_QStdWeb::finallyWithThrowInThen() +{ + SKIP_IF_NO_ASYNCIFY(); + + init(); + + qstdweb::Promise::make(m_testSupport, "makeTestPromise", { + .thenFunc = [](val result) { + Q_UNUSED(result); + EM_ASM({ + throw "Expected error"; + }); + }, + .catchFunc = [](val result) { + QVERIFY(result.isString()); + QCOMPARE("Expected error", result.as<std::string>()); + }, + .finallyFunc = []() { + EM_ASM({ + testSupport.finishWaiting(); + }); + } + }, std::string("bareFinallyWithThen")); + + EM_ASM({ + testSupport.resolve["bareFinallyWithThen"](); + }); + + awaitCondition(); +} + +void tst_QStdWeb::nested() +{ + SKIP_IF_NO_ASYNCIFY(); + + init(); + + qstdweb::Promise::make(m_testSupport, "makeTestPromise", { + .thenFunc = [this](val result) { + QVERIFY(result.isString()); + QCOMPARE("Outer data", result.as<std::string>()); + + qstdweb::Promise::make(m_testSupport, "makeTestPromise", { + .thenFunc = [this](val innerResult) { + QVERIFY(innerResult.isString()); + QCOMPARE("Inner data", innerResult.as<std::string>()); + + qstdweb::Promise::make(m_testSupport, "makeTestPromise", { + .thenFunc = [](val innerResult) { + QVERIFY(innerResult.isString()); + QCOMPARE("Innermost data", innerResult.as<std::string>()); + + EM_ASM({ + testSupport.finishWaiting(); + }); + }, + .catchFunc = [](val error) { + Q_UNUSED(error); + QFAIL("Unexpected catch"); + } + }, std::string("innermost")); + + EM_ASM({ + testSupport.finishWaiting(); + }); + }, + .catchFunc = [](val error) { + Q_UNUSED(error); + QFAIL("Unexpected catch"); + } + }, std::string("inner")); + + EM_ASM({ + testSupport.finishWaiting(); + }); + }, + .catchFunc = [](val error) { + Q_UNUSED(error); + QFAIL("Unexpected catch"); + } + }, std::string("outer")); + + EM_ASM({ + testSupport.resolve["outer"]("Outer data"); + }); + + awaitCondition(); + + EM_ASM({ + testSupport.waitConditionPromise = new Promise((resolve, reject) => { + testSupport.finishWaiting = resolve; + }); + }); + + EM_ASM({ + testSupport.resolve["inner"]("Inner data"); + }); + + awaitCondition(); + + EM_ASM({ + testSupport.waitConditionPromise = new Promise((resolve, reject) => { + testSupport.finishWaiting = resolve; + }); + }); + + EM_ASM({ + testSupport.resolve["innermost"]("Innermost data"); + }); + + awaitCondition(); +} + +void tst_QStdWeb::all() +{ + SKIP_IF_NO_ASYNCIFY(); + + init(); + + val promise1 = m_testSupport.call<val>("makeTestPromise", val("promise1")); + val promise2 = m_testSupport.call<val>("makeTestPromise", val("promise2")); + val promise3 = m_testSupport.call<val>("makeTestPromise", val("promise3")); + + auto thenCalledOnce = std::shared_ptr<bool>(); + *thenCalledOnce = true; + + qstdweb::Promise::all({promise1, promise2, promise3}, { + .thenFunc = [thenCalledOnce](val result) { + QVERIFY(*thenCalledOnce); + *thenCalledOnce = false; + + QVERIFY(result.isArray()); + QCOMPARE(3, result["length"].as<int>()); + QCOMPARE("Data 1", result[0].as<std::string>()); + QCOMPARE("Data 2", result[1].as<std::string>()); + QCOMPARE("Data 3", result[2].as<std::string>()); + + EM_ASM({ + testSupport.finishWaiting(); + }); + }, + .catchFunc = [](val result) { + Q_UNUSED(result); + EM_ASM({ + throw new Error("Unexpected error"); + }); + } + }); + + EM_ASM({ + testSupport.resolve["promise3"]("Data 3"); + testSupport.resolve["promise1"]("Data 1"); + testSupport.resolve["promise2"]("Data 2"); + }); + + awaitCondition(); +} + +void tst_QStdWeb::allWithThrow() +{ + SKIP_IF_NO_ASYNCIFY(); + + init(); + + val promise1 = m_testSupport.call<val>("makeTestPromise", val("promise1")); + val promise2 = m_testSupport.call<val>("makeTestPromise", val("promise2")); + val promise3 = m_testSupport.call<val>("makeTestPromise", val("promise3")); + + auto catchCalledOnce = std::shared_ptr<bool>(); + *catchCalledOnce = true; + + qstdweb::Promise::all({promise1, promise2, promise3}, { + .thenFunc = [](val result) { + Q_UNUSED(result); + QFAIL("Unexpected then"); + }, + .catchFunc = [catchCalledOnce](val result) { + QVERIFY(*catchCalledOnce); + *catchCalledOnce = false; + QVERIFY(result.isString()); + QCOMPARE("Error 2", result.as<std::string>()); + EM_ASM({ + testSupport.finishWaiting(); + }); + } + }); + + EM_ASM({ + testSupport.resolve["promise3"]("Data 3"); + testSupport.resolve["promise1"]("Data 1"); + testSupport.reject["promise2"]("Error 2"); + }); + + awaitCondition(); +} + +void tst_QStdWeb::allWithFinally() +{ + SKIP_IF_NO_ASYNCIFY(); + + init(); + + val promise1 = m_testSupport.call<val>("makeTestPromise", val("promise1")); + val promise2 = m_testSupport.call<val>("makeTestPromise", val("promise2")); + val promise3 = m_testSupport.call<val>("makeTestPromise", val("promise3")); + + auto finallyCalledOnce = std::shared_ptr<bool>(); + *finallyCalledOnce = true; + + qstdweb::Promise::all({promise1, promise2, promise3}, { + .thenFunc = [](val result) { + Q_UNUSED(result); + }, + .finallyFunc = [finallyCalledOnce]() { + QVERIFY(*finallyCalledOnce); + *finallyCalledOnce = false; + EM_ASM({ + testSupport.finishWaiting(); + }); + } + }); + + EM_ASM({ + testSupport.resolve["promise3"]("Data 3"); + testSupport.resolve["promise1"]("Data 1"); + testSupport.resolve["promise2"]("Data 2"); + }); + + awaitCondition(); +} + +void tst_QStdWeb::allWithFinallyAndThrow() +{ + SKIP_IF_NO_ASYNCIFY(); + + init(); + + val promise1 = m_testSupport.call<val>("makeTestPromise", val("promise1")); + val promise2 = m_testSupport.call<val>("makeTestPromise", val("promise2")); + val promise3 = m_testSupport.call<val>("makeTestPromise", val("promise3")); + + auto finallyCalledOnce = std::shared_ptr<bool>(); + *finallyCalledOnce = true; + + qstdweb::Promise::all({promise1, promise2, promise3}, { + .thenFunc = [](val result) { + Q_UNUSED(result); + EM_ASM({ + throw "This breaks it all!!!"; + }); + }, + .finallyFunc = [finallyCalledOnce]() { + QVERIFY(*finallyCalledOnce); + *finallyCalledOnce = false; + EM_ASM({ + testSupport.finishWaiting(); + }); + } + }); + + EM_ASM({ + testSupport.resolve["promise3"]("Data 3"); + testSupport.resolve["promise1"]("Data 1"); + testSupport.resolve["promise2"]("Data 2"); + }); + + awaitCondition(); +} + +QTEST_APPLESS_MAIN(tst_QStdWeb) +#include "tst_qstdweb.moc" |