diff options
Diffstat (limited to 'tests/manual/wasm/qstdweb')
-rw-r--r-- | tests/manual/wasm/qstdweb/CMakeLists.txt | 98 | ||||
-rw-r--r-- | tests/manual/wasm/qstdweb/files_auto.html | 13 | ||||
-rw-r--r-- | tests/manual/wasm/qstdweb/files_main.cpp | 471 | ||||
-rw-r--r-- | tests/manual/wasm/qstdweb/iodevices_auto.html | 10 | ||||
-rw-r--r-- | tests/manual/wasm/qstdweb/iodevices_main.cpp | 103 | ||||
-rw-r--r-- | tests/manual/wasm/qstdweb/promise_auto.html | 10 | ||||
-rw-r--r-- | tests/manual/wasm/qstdweb/promise_main.cpp | 486 | ||||
-rw-r--r-- | tests/manual/wasm/qstdweb/qwasmcompositor_auto.html | 10 | ||||
-rw-r--r-- | tests/manual/wasm/qstdweb/qwasmcompositor_main.cpp | 172 |
9 files changed, 1373 insertions, 0 deletions
diff --git a/tests/manual/wasm/qstdweb/CMakeLists.txt b/tests/manual/wasm/qstdweb/CMakeLists.txt new file mode 100644 index 0000000000..39039c3910 --- /dev/null +++ b/tests/manual/wasm/qstdweb/CMakeLists.txt @@ -0,0 +1,98 @@ +# Copyright (C) 2024 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause +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" |