From 50216b65daaa6ae36b0aa39ace269259413e8bca Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sat, 16 Sep 2017 17:56:01 +0200 Subject: [PATCH] - wip - --- .gitignore | 1 + include/QtQmlPromise | 6 + qtpromise.pri | 8 + qtpromise.pro | 5 +- src/imports/imports.pro | 25 ++ src/imports/imports.qrc | 5 + src/imports/plugin.cpp | 76 ++++ src/imports/plugins.qmltypes | 5 + src/imports/qmldir | 5 + src/imports/qtqmlpromise.js | 16 + src/qtpromise/qtpromise.pro | 4 +- src/qtqmlpromise/qjspromise.cpp | 175 ++++++++ src/qtqmlpromise/qjspromise.h | 52 +++ src/qtqmlpromise/qjspromise_p.h | 71 ++++ src/qtqmlpromise/qtqmlpromise.pri | 7 + src/qtqmlpromise/qtqmlpromise.pro | 8 + src/qtqmlpromise/qtqmlpromiseglobal.h | 16 + src/src.pro | 5 +- tests/auto/auto.pro | 4 +- .../auto/qtqmlpromise/extension/extension.pro | 5 + .../qtqmlpromise/extension/tst_extension.cpp | 10 + .../qtqmlpromise/extension/tst_extension.qml | 39 ++ tests/auto/qtqmlpromise/qtqmlpromise.pri | 9 + tests/auto/qtqmlpromise/qtqmlpromise.pro | 4 + .../requirements/requirements.pro | 5 + .../requirements/tst_requirements.cpp | 10 + .../requirements/tst_requirements.qml | 384 ++++++++++++++++++ 27 files changed, 953 insertions(+), 7 deletions(-) create mode 100644 include/QtQmlPromise create mode 100644 src/imports/imports.pro create mode 100644 src/imports/imports.qrc create mode 100644 src/imports/plugin.cpp create mode 100644 src/imports/plugins.qmltypes create mode 100644 src/imports/qmldir create mode 100644 src/imports/qtqmlpromise.js create mode 100644 src/qtqmlpromise/qjspromise.cpp create mode 100644 src/qtqmlpromise/qjspromise.h create mode 100644 src/qtqmlpromise/qjspromise_p.h create mode 100644 src/qtqmlpromise/qtqmlpromise.pri create mode 100644 src/qtqmlpromise/qtqmlpromise.pro create mode 100644 src/qtqmlpromise/qtqmlpromiseglobal.h create mode 100644 tests/auto/qtqmlpromise/extension/extension.pro create mode 100644 tests/auto/qtqmlpromise/extension/tst_extension.cpp create mode 100644 tests/auto/qtqmlpromise/extension/tst_extension.qml create mode 100644 tests/auto/qtqmlpromise/qtqmlpromise.pri create mode 100644 tests/auto/qtqmlpromise/qtqmlpromise.pro create mode 100644 tests/auto/qtqmlpromise/requirements/requirements.pro create mode 100644 tests/auto/qtqmlpromise/requirements/tst_requirements.cpp create mode 100644 tests/auto/qtqmlpromise/requirements/tst_requirements.qml diff --git a/.gitignore b/.gitignore index 8281286..e6d3196 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ node_modules *.obj *.exe *.user +*.qmlc Makefile* moc_*.cpp moc_*.h diff --git a/include/QtQmlPromise b/include/QtQmlPromise new file mode 100644 index 0000000..552a824 --- /dev/null +++ b/include/QtQmlPromise @@ -0,0 +1,6 @@ +#ifndef QTQMLPROMISE_MODULE_H +#define QTQMLPROMISE_MODULE_H + +#include "../src/qtqmlpromise/qjspromise.h" + +#endif // ifndef QTQMLPROMISE_MODULE_H diff --git a/qtpromise.pri b/qtpromise.pri index d3302a8..a9a4a59 100644 --- a/qtpromise.pri +++ b/qtpromise.pri @@ -1,3 +1,11 @@ INCLUDEPATH += $$PWD/include $$PWD/src DEPENDPATH += $$PWD/include $$PWD/src CONFIG += c++11 + +qtpromise-qml { + QML_IMPORT_PATH += $$shadowed($$PWD)/qml + + # To avoid carrying an extra library dependency, the QJSPromise definition is + # embedded in the QML plugin, so we need to link against the plugin itself. + LIBS += -L$$shadowed($$PWD)/qml/QtPromise -l$$qtLibraryTarget(qtpromiseplugin) +} diff --git a/qtpromise.pro b/qtpromise.pro index 2475104..b7d790e 100644 --- a/qtpromise.pro +++ b/qtpromise.pro @@ -1,10 +1,9 @@ TEMPLATE = subdirs SUBDIRS = \ + src \ tests -_qt_creator_ { - SUBDIRS += src -} +tests.depends = src OTHER_FILES = \ package/features/*.prf \ diff --git a/src/imports/imports.pro b/src/imports/imports.pro new file mode 100644 index 0000000..eeb58d4 --- /dev/null +++ b/src/imports/imports.pro @@ -0,0 +1,25 @@ +TEMPLATE = lib +CONFIG += plugin exceptions +QT += qml + +IMPORT_VERSION = 1.0 +DEFINES += QTQMLPROMISE_LIBRARY + +TARGET = $$qtLibraryTarget(qtpromiseplugin) +DESTDIR = $$shadowed($$PWD/../../qml/QtPromise) + +include(../qtqmlpromise/qtqmlpromise.pri) +include(../../qtpromise.pri) + +SOURCES += \ + $$PWD/plugin.cpp + +QMLFILES += \ + $$PWD/plugins.qmltypes \ + $$PWD/qmldir + +RESOURCES += $$PWD/imports.qrc + +qmlfiles.files = $$QMLFILES +qmlfiles.path = $$DESTDIR +COPIES += qmlfiles diff --git a/src/imports/imports.qrc b/src/imports/imports.qrc new file mode 100644 index 0000000..4a2a6f9 --- /dev/null +++ b/src/imports/imports.qrc @@ -0,0 +1,5 @@ + + + qtqmlpromise.js + + diff --git a/src/imports/plugin.cpp b/src/imports/plugin.cpp new file mode 100644 index 0000000..0b33a4c --- /dev/null +++ b/src/imports/plugin.cpp @@ -0,0 +1,76 @@ +// QtPromise +#include + +// Qt +#include +#include + +static const char* kJSPromisePrivateNamespace = "__qtpromise_private__"; + +using namespace QtPromise; + +class QtQmlPromiseObject : public QObject +{ + Q_OBJECT + +public: + QtQmlPromiseObject(QJSEngine* engine) + : QObject(engine) + , m_engine(engine) + { + Q_ASSERT(engine); + } + + Q_INVOKABLE QJSValue create(const QJSValue& resolver, const QJSValue& prototype) + { + QJSValue value = m_engine->toScriptValue(QJSPromise(m_engine, resolver)); + value.setPrototype(prototype); + return value; + } + + Q_INVOKABLE QtPromise::QJSPromise resolve(QJSValue value) + { + return QJSPromise::resolve(std::move(value)); + } + + Q_INVOKABLE QtPromise::QJSPromise reject(QJSValue error) + { + return QJSPromise::reject(std::move(error)); + } + + Q_INVOKABLE QtPromise::QJSPromise all(QJSValue input) + { + return QJSPromise::all(m_engine, std::move(input)); + } + +private: + QJSEngine* m_engine; +}; + +class QtQmlPromisePlugin : public QQmlExtensionPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID QQmlExtensionInterface_iid) + +public: + void registerTypes(const char* uri) Q_DECL_OVERRIDE + { + Q_ASSERT(QLatin1String(uri) == QLatin1String("QtPromise")); + Q_UNUSED(uri); + + qRegisterMetaType(); + } + + void initializeEngine(QQmlEngine* engine, const char* uri) Q_DECL_OVERRIDE + { + Q_ASSERT(QLatin1String(uri) == QLatin1String("QtPromise")); + Q_UNUSED(uri); + + QJSValue global = engine->globalObject(); + QJSValue object = engine->newQObject(new QtQmlPromiseObject(engine)); + global.setProperty(kJSPromisePrivateNamespace, object); + } + +}; // class QtQmlPromisePlugin + +#include "plugin.moc" diff --git a/src/imports/plugins.qmltypes b/src/imports/plugins.qmltypes new file mode 100644 index 0000000..cd4e7a3 --- /dev/null +++ b/src/imports/plugins.qmltypes @@ -0,0 +1,5 @@ +import QtQuick.tooling 1.2 + +Module { + dependencies: [] +} diff --git a/src/imports/qmldir b/src/imports/qmldir new file mode 100644 index 0000000..8bd4542 --- /dev/null +++ b/src/imports/qmldir @@ -0,0 +1,5 @@ +module QtPromise +plugin qtpromiseplugin +typeinfo plugins.qmltypes +classname QtQmlPromisePlugin +Promise 1.0 qrc:///QtPromise/qtqmlpromise.js diff --git a/src/imports/qtqmlpromise.js b/src/imports/qtqmlpromise.js new file mode 100644 index 0000000..5ded053 --- /dev/null +++ b/src/imports/qtqmlpromise.js @@ -0,0 +1,16 @@ +.pragma library + +(function(global) { + var private = global.__qtpromise_private__; + delete global.__qtpromise_private__; + + var Promise = global.Promise = function(resolver) { + return private.create(function(proxy) { + resolver(proxy.resolve, proxy.reject); + }, this); + }; + + ['all', 'reject', 'resolve'].forEach(function(method) { + Promise[method] = private[method]; + }); +})(this); diff --git a/src/qtpromise/qtpromise.pro b/src/qtpromise/qtpromise.pro index 71afe76..8b7e952 100644 --- a/src/qtpromise/qtpromise.pro +++ b/src/qtpromise/qtpromise.pro @@ -1,5 +1,5 @@ -TEMPLATE = lib -CONFIG += c++11 qt thread warn_on +TEMPLATE = aux +CONFIG += c++11 force_qt thread warn_on DEFINES += QT_DEPRECATED_WARNINGS include(qtpromise.pri) diff --git a/src/qtqmlpromise/qjspromise.cpp b/src/qtqmlpromise/qjspromise.cpp new file mode 100644 index 0000000..eb53f38 --- /dev/null +++ b/src/qtqmlpromise/qjspromise.cpp @@ -0,0 +1,175 @@ +// QtQmlPromise +#include "qjspromise.h" +#include "qjspromise_p.h" + +// Qt +#include +#include + +using namespace QtPromise; + +namespace { + +QJSValue toJSArray(QJSEngine* engine, const QVector& values) +{ + Q_ASSERT(engine); + + const int count = values.count(); + QJSValue array = engine->newArray(count); + + for (int i = 0; i < count; ++i) { + array.setProperty(i, values[i]); + } + + return array; +} + +template +QJSValue newError(const QJSValue& source, const T& value) +{ + QJSEngine* engine = source.engine(); + QJSValue error = engine->evaluate("throw new Error('')"); + error.setProperty("message", engine->toScriptValue(value)); + return error; +} + +} // anonymous namespace + +QJSPromise::QJSPromise() + : m_promise(QPromise::resolve(QJSValue())) +{ } + +QJSPromise::QJSPromise(QPromise&& promise) + : m_promise(std::move(promise)) +{ +} + +QJSPromise::QJSPromise(QJSEngine* engine, QJSValue resolver) + : m_promise([=]( + const QPromiseResolve& resolve, + const QPromiseReject& reject) mutable { + + // resolver is part of the Promise wrapper in qtqmlpromise.js + Q_ASSERT(resolver.isCallable()); + Q_ASSERT(engine); + + auto proxy = QtPromisePrivate::JSPromiseResolver(resolve, reject); + auto ret = resolver.call(QJSValueList() << engine->toScriptValue(proxy)); + if (ret.isError()) { + throw ret; + } + }) +{ } + +QJSPromise QJSPromise::then(QJSValue fulfilled, QJSValue rejected) const +{ + const bool fulfillable = fulfilled.isCallable(); + const bool rejectable = rejected.isCallable(); + + if (!fulfillable && !rejectable) { + return *this; + } + + auto _rejected = [=]() mutable { + QJSValue error; + + try { + throw; + } catch (const QJSValue& err) { + error = err; + } catch (const QVariant& err) { + error = newError(rejected, err); + } catch (const std::exception& e) { + error = newError(rejected, QString(e.what())); + } catch (...) { + error = newError(rejected, QString("Unknown error")); + } + + return rejected.call({error}); + }; + + if (!fulfillable) { + return m_promise.then(nullptr, _rejected); + } + + auto _fulfilled = [=](const QJSValue& res) mutable { + return fulfilled.call(QJSValueList() << res); + }; + + if (!rejectable) { + return m_promise.then(_fulfilled); + } + + return m_promise.then(_fulfilled, _rejected); +} + +QJSPromise QJSPromise::fail(QJSValue handler) const +{ + return then(QJSValue(), handler); +} + +QJSPromise QJSPromise::finally(QJSValue handler) const +{ + return m_promise.finally([=]() mutable { + return handler.call(); + }); +} + +QJSPromise QJSPromise::tap(QJSValue handler) const +{ + return m_promise.tap([=](const QJSValue& res) mutable { + return handler.call(QJSValueList() << res); + }); +} + +QJSPromise QJSPromise::delay(int msec) const +{ + return m_promise.delay(msec); +} + +QJSPromise QJSPromise::wait() const +{ + return m_promise.wait(); +} + +QJSPromise QJSPromise::resolve(QJSValue&& value) +{ + return QPromise::resolve(std::forward(value)); +} + +QJSPromise QJSPromise::reject(QJSValue&& error) +{ + return QPromise::reject(std::forward(error)); +} + +QJSPromise QJSPromise::all(QJSEngine* engine, QJSValue&& input) +{ + Q_ASSERT(engine); + + if (!input.isArray()) { + // TODO TYPEERROR! + return QPromise::reject("foobar"); + } + + Q_ASSERT(input.hasProperty("length")); + const int count = input.property("length").toInt(); + if (!count) { + return QPromise::resolve(QJSValue(input)); + } + + QList > promises; + for (int i = 0; i < count; ++i) { + QJSValue value = input.property(i); + const QVariant variant = value.toVariant(); + if (variant.userType() == qMetaTypeId()) { + promises << variant.value().m_promise; + } else { + promises << QPromise::resolve(std::move(value)); + } + } + + return QPromise::all(promises) + .then([engine](const QVector& results) { + return toJSArray(engine, results); + }); +} diff --git a/src/qtqmlpromise/qjspromise.h b/src/qtqmlpromise/qjspromise.h new file mode 100644 index 0000000..963a9c4 --- /dev/null +++ b/src/qtqmlpromise/qjspromise.h @@ -0,0 +1,52 @@ +#ifndef QTQMLPROMISE_QJSPROMISE_H +#define QTQMLPROMISE_QJSPROMISE_H + +// QtQmlPromise +#include "qtqmlpromiseglobal.h" + +// QtPromise +#include + +// Qt +#include + +namespace QtPromise { + +class QTQMLPROMISE_EXPORT QJSPromise +{ + Q_GADGET + +public: + QJSPromise(); + QJSPromise(QJSEngine* engine, QJSValue resolver); + QJSPromise(QPromise&& promise); + + Q_INVOKABLE bool isFulfilled() const { return m_promise.isFulfilled(); } + Q_INVOKABLE bool isRejected() const { return m_promise.isRejected(); } + Q_INVOKABLE bool isPending() const{ return m_promise.isPending(); } + + Q_INVOKABLE QtPromise::QJSPromise then(QJSValue fulfilled, QJSValue rejected = QJSValue()) const; + Q_INVOKABLE QtPromise::QJSPromise fail(QJSValue handler) const; + Q_INVOKABLE QtPromise::QJSPromise finally(QJSValue handler) const; + Q_INVOKABLE QtPromise::QJSPromise tap(QJSValue handler) const; + Q_INVOKABLE QtPromise::QJSPromise delay(int msec) const; + Q_INVOKABLE QtPromise::QJSPromise wait() const; + +public: // STATICS + static QJSPromise resolve(QJSValue&& value); + static QJSPromise reject(QJSValue&& error); + static QJSPromise all(QJSEngine* engine, QJSValue&& input); + +private: + friend struct QtPromisePrivate::PromiseFulfill; + + QPromise m_promise; + +}; // class QJSPromise + +} // namespace QtPromise + +Q_DECLARE_TYPEINFO(QtPromise::QJSPromise, Q_MOVABLE_TYPE); +Q_DECLARE_METATYPE(QtPromise::QJSPromise) + +#endif // ifndef QTQMLPROMISE_QJSPROMISE_H diff --git a/src/qtqmlpromise/qjspromise_p.h b/src/qtqmlpromise/qjspromise_p.h new file mode 100644 index 0000000..26cd6da --- /dev/null +++ b/src/qtqmlpromise/qjspromise_p.h @@ -0,0 +1,71 @@ +#ifndef QTQMLPROMISE_QJSPROMISE_P_H +#define QTQMLPROMISE_QJSPROMISE_P_H + +// QtQmlPromise +#include "qjspromise.h" + +// QtPromise +#include + +// Qt +#include + +namespace QtPromisePrivate { + +template <> +struct PromiseFulfill +{ + static void call( + const QJSValue& value, + const QtPromise::QPromiseResolve& resolve, + const QtPromise::QPromiseReject& reject) + { + using namespace QtPromise; + + if (value.isObject()) { + const QVariant variant = value.toVariant(); + if (variant.userType() == qMetaTypeId()) { + const auto promise = variant.value().m_promise; + PromiseFulfill >::call(promise, resolve, reject); + return; + } + } + + if (value.isError()) { + reject(value); + } else { + resolve(value); + } + } +}; + +class JSPromiseResolver +{ + Q_GADGET + +public: + JSPromiseResolver() {} + JSPromiseResolver( + const QtPromise::QPromiseResolve& resolve, + const QtPromise::QPromiseReject& reject) + : m_d(new Data{resolve, reject}) + { } + + Q_INVOKABLE void resolve(QJSValue value = QJSValue()) { m_d->resolve(std::move(value)); } + Q_INVOKABLE void reject(QJSValue error = QJSValue()) { m_d->reject(std::move(error)); } + +private: + struct Data + { + QtPromise::QPromiseResolve resolve; + QtPromise::QPromiseReject reject; + }; // struct Data + + QSharedPointer m_d; +}; + +} // namespace QtPromisePrivate + +Q_DECLARE_METATYPE(QtPromisePrivate::JSPromiseResolver) + +#endif // ifndef QTQMLPROMISE_QJSPROMISE_P_H diff --git a/src/qtqmlpromise/qtqmlpromise.pri b/src/qtqmlpromise/qtqmlpromise.pri new file mode 100644 index 0000000..d80922e --- /dev/null +++ b/src/qtqmlpromise/qtqmlpromise.pri @@ -0,0 +1,7 @@ +HEADERS += \ + $$PWD/qjspromise.h \ + $$PWD/qjspromise_p.h \ + $$PWD/qtqmlpromiseglobal.h + +SOURCES += \ + $$PWD/qjspromise.cpp diff --git a/src/qtqmlpromise/qtqmlpromise.pro b/src/qtqmlpromise/qtqmlpromise.pro new file mode 100644 index 0000000..61f306d --- /dev/null +++ b/src/qtqmlpromise/qtqmlpromise.pro @@ -0,0 +1,8 @@ +TEMPLATE = aux +CONFIG += c++11 force_qt thread warn_on +DEFINES += QT_DEPRECATED_WARNINGS +DEFINES += QTQMLPROMISE_LIBRARY +QT += qml + +include($$PWD/../../qtpromise.pri) +include($$PWD/qtqmlpromise.pri) diff --git a/src/qtqmlpromise/qtqmlpromiseglobal.h b/src/qtqmlpromise/qtqmlpromiseglobal.h new file mode 100644 index 0000000..10f7471 --- /dev/null +++ b/src/qtqmlpromise/qtqmlpromiseglobal.h @@ -0,0 +1,16 @@ +#ifndef QTQMLPROMISE_QTQMLPROMISEGLOBAL_H +#define QTQMLPROMISE_QTQMLPROMISEGLOBAL_H + +// QtPromise +#include + +// QtCore +#include + +#ifdef QTQMLPROMISE_LIBRARY +# define QTQMLPROMISE_EXPORT Q_DECL_EXPORT +#else +# define QTQMLPROMISE_EXPORT Q_DECL_IMPORT +#endif + +#endif // ifndef QTQMLPROMISE_QTQMLPROMISEGLOBAL_H diff --git a/src/src.pro b/src/src.pro index 720a606..4f1a84d 100644 --- a/src/src.pro +++ b/src/src.pro @@ -1,2 +1,5 @@ TEMPLATE = subdirs -SUBDIRS = qtpromise +SUBDIRS += \ + imports \ + qtpromise \ + qtqmlpromise diff --git a/tests/auto/auto.pro b/tests/auto/auto.pro index bfff61e..c3500fa 100644 --- a/tests/auto/auto.pro +++ b/tests/auto/auto.pro @@ -1,2 +1,4 @@ TEMPLATE = subdirs -SUBDIRS += qtpromise +SUBDIRS += \ + qtpromise \ + qtqmlpromise diff --git a/tests/auto/qtqmlpromise/extension/extension.pro b/tests/auto/qtqmlpromise/extension/extension.pro new file mode 100644 index 0000000..a0f19f9 --- /dev/null +++ b/tests/auto/qtqmlpromise/extension/extension.pro @@ -0,0 +1,5 @@ +TARGET = tst_qtqmlpromise_extension +SOURCES += $$PWD/tst_extension.cpp +OTHER_FILES += $$PWD/*.qml + +include(../qtqmlpromise.pri) diff --git a/tests/auto/qtqmlpromise/extension/tst_extension.cpp b/tests/auto/qtqmlpromise/extension/tst_extension.cpp new file mode 100644 index 0000000..ae393e5 --- /dev/null +++ b/tests/auto/qtqmlpromise/extension/tst_extension.cpp @@ -0,0 +1,10 @@ +#include + +static void initialize() +{ + qputenv("QML2_IMPORT_PATH", QTPROMISE_IMPORTPATH); +} + +Q_COREAPP_STARTUP_FUNCTION(initialize) + +QUICK_TEST_MAIN(extension) diff --git a/tests/auto/qtqmlpromise/extension/tst_extension.qml b/tests/auto/qtqmlpromise/extension/tst_extension.qml new file mode 100644 index 0000000..892c8d4 --- /dev/null +++ b/tests/auto/qtqmlpromise/extension/tst_extension.qml @@ -0,0 +1,39 @@ +import QtQuick 2.3 +import QtPromise 1.0 +import QtTest 1.0 + +TestCase { + name: "Extension" + + function test_global() { + compare(typeof __qtpromise_private__, 'undefined'); + compare(typeof Promise, 'function'); + compare(typeof Promise.resolve, 'function'); + compare(typeof Promise.reject, 'function'); + } + + function test_instance() { + var p = new Promise(function() {}); + compare(Object.prototype.toString(p), '[object Object]'); + compare(p instanceof Promise, true); + compare(typeof p, 'object'); + } + + function test_prototype() { + var p = new Promise(function() {}); + + [ + 'delay', + 'fail', + 'finally', + 'isFulfilled', + 'isRejected', + 'isPending', + 'tap', + 'then', + 'wait', + ].forEach(function(name) { + compare(typeof p[name], 'function'); + }); + } +} diff --git a/tests/auto/qtqmlpromise/qtqmlpromise.pri b/tests/auto/qtqmlpromise/qtqmlpromise.pri new file mode 100644 index 0000000..7b9fb7b --- /dev/null +++ b/tests/auto/qtqmlpromise/qtqmlpromise.pri @@ -0,0 +1,9 @@ +TEMPLATE = app +CONFIG += qmltestcase +CONFIG += qtpromise-qml +DEFINES += QT_DEPRECATED_WARNINGS + +QTPROMISE_IMPORTPATH = $$shadowed($$PWD/../../../qml) +DEFINES += QTPROMISE_IMPORTPATH=\"\\\"$$QTPROMISE_IMPORTPATH\\\"\" + +include(../../../qtpromise.pri) diff --git a/tests/auto/qtqmlpromise/qtqmlpromise.pro b/tests/auto/qtqmlpromise/qtqmlpromise.pro new file mode 100644 index 0000000..431995f --- /dev/null +++ b/tests/auto/qtqmlpromise/qtqmlpromise.pro @@ -0,0 +1,4 @@ +TEMPLATE = subdirs +SUBDIRS += \ + extension \ + requirements diff --git a/tests/auto/qtqmlpromise/requirements/requirements.pro b/tests/auto/qtqmlpromise/requirements/requirements.pro new file mode 100644 index 0000000..dc504ef --- /dev/null +++ b/tests/auto/qtqmlpromise/requirements/requirements.pro @@ -0,0 +1,5 @@ +TARGET = tst_qtqmlpromise_requirements +SOURCES += $$PWD/tst_requirements.cpp +OTHER_FILES += $$PWD/*.qml + +include(../qtqmlpromise.pri) diff --git a/tests/auto/qtqmlpromise/requirements/tst_requirements.cpp b/tests/auto/qtqmlpromise/requirements/tst_requirements.cpp new file mode 100644 index 0000000..3c59f74 --- /dev/null +++ b/tests/auto/qtqmlpromise/requirements/tst_requirements.cpp @@ -0,0 +1,10 @@ +#include + +static void initialize() +{ + qputenv("QML2_IMPORT_PATH", QTPROMISE_IMPORTPATH); +} + +Q_COREAPP_STARTUP_FUNCTION(initialize) + +QUICK_TEST_MAIN(requirements) diff --git a/tests/auto/qtqmlpromise/requirements/tst_requirements.qml b/tests/auto/qtqmlpromise/requirements/tst_requirements.qml new file mode 100644 index 0000000..26824c1 --- /dev/null +++ b/tests/auto/qtqmlpromise/requirements/tst_requirements.qml @@ -0,0 +1,384 @@ +import QtQuick 2.3 +import QtPromise 1.0 +import QtTest 1.0 + +// https://promisesaplus.com/#requirements +TestCase { + name: "Requirements" + + // 2.1. Promise States + + // 2.1.1 When pending, a promise: + function test_pendingFulfilled() { + // 2.1.1.1. may transition to either the fulfilled state + var p = new Promise(function(resolve) { + Qt.callLater(function() { + resolve(42); + }); + }); + + compare(p.isPending(), true); + compare(p.isFulfilled(), false); + compare(p.isRejected(), false); + + p.wait(); + + compare(p.isPending(), false); + compare(p.isFulfilled(), true); + compare(p.isRejected(), false); + } + + // 2.1.1 When pending, a promise: + function test_pendingRejected() { + // 2.1.1.1. ... or the rejected state + var p = new Promise(function(resolve, reject) { + Qt.callLater(function() { + reject(new Error(42)); + }); + }); + + compare(p.isPending(), true); + compare(p.isFulfilled(), false); + compare(p.isRejected(), false); + + p.wait(); + + compare(p.isPending(), false); + compare(p.isFulfilled(), false); + compare(p.isRejected(), true); + } + + // 2.1.2. When fulfilled, a promise: + function test_fulfilled() { + var p = new Promise(function(resolve, reject) { + Qt.callLater(function() { + // 2.1.2.2. must have a value, which must not change. + resolve(42); + resolve(43); + + // 2.1.2.1. must not transition to any other state. + reject(new Error("foo")); + }); + }); + + compare(p.isPending(), true); + + var value = -1; + var error = null; + + p.then(function(res) { + value = res; + }, function(err) { + error = err; + }).wait(); + + compare(p.isFulfilled(), true); + compare(p.isRejected(), false); + compare(error, null); + compare(value, 42); + } + + // 2.1.3 When rejected, a promise: + function test_rejected() { + var p = new Promise(function(resolve, reject) { + Qt.callLater(function() { + // 2.1.3.2. must have a reason, which must not change. + reject(new Error("foo")); + reject(new Error("bar")); + + // 2.1.3.1. must not transition to any other state. + resolve(42); + }); + }); + + compare(p.isPending(), true); + + var value = -1; + var error = null; + + p.then(function(res) { + value = res; + }, function(err) { + error = err; + }).wait(); + + compare(p.isFulfilled(), false); + compare(p.isRejected(), true); + compare(error instanceof Error, true); + compare(error.message, 'foo'); + compare(value, -1); + } + + // 2.2. The then Method + + // 2.2.1. Both onFulfilled and onRejected are given (resolve) + function test_thenArgsBothResolve() { + var error = null; + var value = -1; + + Promise.resolve(42).then( + function(res) { value = res; }, + function(err) { error = err; } + ).wait(); + + compare(error, null); + compare(value, 42); + } + + // 2.2.1. Both onFulfilled and onRejected are given (reject) + function test_thenArgsBothReject() { + var error = null; + var value = -1; + + Promise.reject(new Error('foo')).then( + function(res) { value = res; }, + function(err) { error = err; } + ).wait(); + + compare(error instanceof Error, true); + compare(error.message, 'foo'); + compare(value, -1); + } + + // 2.2.1. onFulfilled is an optional arguments: + function test_thenArgsSecond() { + var error = null; + + Promise.reject(new Error('foo')).then( + null, + function(err) { error = err; } + ).wait(); + + compare(error instanceof Error, true); + compare(error.message, 'foo'); + } + + // 2.2.1. onRejected is an optional arguments: + function test_thenArgsFirst() { + var value = -1; + + Promise.resolve(42).then( + function(res) { value = res; } + ).wait(); + + compare(value, 42); + } + + // 2.2.1.1. If onFulfilled is not a function, it must be ignored. + function test_thenArgsFirstInvalid() { + var value = -1; + + Promise.resolve(42).then('invalid').then( + function(res) { value = res; } + ).wait(); + + compare(value, 42); + } + + // 2.2.1.2. If onRejected is not a function, it must be ignored. + function test_thenArgsSecondInvalid() { + var error = -1; + + Promise.reject(new Error('foo')).then( + null, + 'invalid' + ).then( + null, + function(err) { error = err; } + ).wait(); + + compare(error instanceof Error, true); + compare(error.message, 'foo'); + } + + // 2.2.2. If onFulfilled is a function: + function test_thenOnFulfilled() { + var p0 = new Promise(function(resolve) { + Qt.callLater(function() { + // 2.2.2.3. it must not be called more than once + resolve(42); + resolve(43); + }); + }); + + var values = []; + var p1 = p0.then(function(res) { + values.push(res); + }); + + // 2.2.2.2. it must not be called before promise is fulfilled. + compare(p0.isPending(), true); + compare(p1.isPending(), true); + compare(values.length, 0); + + p1.wait(); + + // 2.2.2.1. it must be called after promise is fulfilled, + // with promise’s value as its first argument. + compare(p0.isFulfilled(), true); + compare(p1.isFulfilled(), true); + compare(values, [42]); + } + + // 2.2.3. If onRejected is a function: + function test_thenOnRejected() { + var p0 = new Promise(function(resolve, reject) { + Qt.callLater(function() { + // 2.2.2.3. it must not be called more than once + reject(new Error('foo')); + reject(new Error('bar')); + }); + }); + + var errors = []; + var p1 = p0.then(null, function(res) { + errors.push(res); + }); + + // 2.2.3.2. it must not be called before promise is rejected. + compare(p0.isPending(), true); + compare(p1.isPending(), true); + compare(errors.length, 0); + + p1.wait(); + + // 2.2.3.1. it must be called after promise is rejected, + // with promise’s reason as its first argument. + compare(p0.isRejected(), true); + compare(p1.isFulfilled(), true); + compare(errors.length, 1); + compare(errors[0] instanceof Error, true); + compare(errors[0].message, 'foo'); + } + + // 2.2.4. onFulfilled or onRejected must not be called until the execution context + // stack contains only platform code (ie. executed asynchronously, after the event + // loop turn in which then is called, and with a fresh stack). + function test_thenAsynchronous() + { + var value = -1; + var p0 = Promise.resolve(42); + var p1 = p0.then(function(res){ value = res; }); + + compare(p0.isFulfilled(), true); + compare(p1.isPending(), true); + compare(value, -1); + + p1.wait(); + + compare(p1.isFulfilled(), true); + compare(value, 42); + } + + // 2.2.5 onFulfilled and onRejected must be called as functions (i.e. with no this value). + function test_thenThisArg() { + var scopes = []; + + Promise.resolve(42).then(function() { scopes.push(this); }).wait(); + Promise.reject(new Error('foo')).then(null, function() { scopes.push(this); }).wait(); + + // Qt doesn't allow to call JS function with undefined "this" + // Let's adopt the sloppy mode (this === the global object). + var global = (function() { return this; })(); + compare(scopes, [global, global]); + } + + // 2.2.6. then may be called multiple times on the same promise: + + // 2.2.6.1. If/when promise is fulfilled, all respective onFulfilled callbacks + // must execute in the order of their originating calls to then: + function test_thenMultipleCalls() { + var values = []; + var p = new Promise(function(resolve) { + Qt.callLater(function() { + resolve(42); + }); + }); + + Promise.all([ + p.then(function(res) { return res + 1; }), + p.then(function(res) { return res + 2; }), + p.then(function(res) { return res + 3; }) + ]).then(function(res) { + values = res; + }).wait(); + + compare(values, [43, 44, 45]); + } +} + +/* +void tst_requirements::thenMultipleCalls() +{ + // 2.2.6.2. If/when promise is rejected, all respective onRejected callbacks + // must execute in the order of their originating calls to then: + { + QVector values; + QPromise p([](const QPromiseResolve&, const QPromiseReject& reject) { + qtpromise_defer([=]() { + reject(8); + }); + }); + + qPromiseAll(QVector >{ + p.then(nullptr, [&](int r) { values << r + 1; return r + 1; }), + p.then(nullptr, [&](int r) { values << r + 2; return r + 2; }), + p.then(nullptr, [&](int r) { values << r + 3; return r + 3; }) + }).wait(); + + QCOMPARE(values, QVector({9, 10, 11})); + } +} + +void tst_requirements::thenHandlers() +{ + // 2.2.7. then must return a promise: p2 = p1.then(onFulfilled, onRejected); + { + auto handler = [](){ return 42; }; + auto p1 = QPromise::resolve(42); + Q_STATIC_ASSERT((std::is_same >::value)); + Q_STATIC_ASSERT((std::is_same >::value)); + Q_STATIC_ASSERT((std::is_same >::value)); + } + + // 2.2.7.1. If either onFulfilled or onRejected returns a value x, run the + // Promise Resolution Procedure [[Resolve]](p2, x) -> See 2.3. + + // 2.2.7.2. If either onFulfilled or onRejected throws an exception e, + // p2 must be rejected with e as the reason. + { + QString reason; + auto p1 = QPromise::resolve(42); + auto p2 = p1.then([](){ throw QString("foo"); }); + p2.then(nullptr, [&](const QString& e) { reason = e; }).wait(); + + QVERIFY(p1.isFulfilled()); + QVERIFY(p2.isRejected()); + QCOMPARE(reason, QString("foo")); + } + { + QString reason; + auto p1 = QPromise::reject(QString("foo")); + auto p2 = p1.then(nullptr, [](){ throw QString("bar"); return 42; }); + p2.then(nullptr, [&](const QString& e) { reason = e; return 0; }).wait(); + + QVERIFY(p1.isRejected()); + QVERIFY(p2.isRejected()); + QCOMPARE(reason, QString("bar")); + } + + // 2.2.7.3. If onFulfilled is not a function and promise1 is fulfilled, + // p2 must be fulfilled with the same value as promise1 + { + QString value; + auto p1 = QPromise::resolve("42"); + auto p2 = p1.then(nullptr, [](){ return QString(); }); + Q_STATIC_ASSERT((std::is_same >::value)); + p2.then([&](const QString& e) { value = e; }).wait(); + + QVERIFY(p1.isFulfilled()); + QVERIFY(p2.isFulfilled()); + QCOMPARE(value, QString("42")); + } +} +*/