From f8e460b9151ee03195dd886c58144b74b01f33f8 Mon Sep 17 00:00:00 2001 From: Mikolaj Boc Date: Mon, 4 Jul 2022 09:57:27 +0200 Subject: Use the local file APIs to save/load files on WASM MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QFileDialog::saveFileContent, QFileDialog::getOpenFileContent are now using local file APIs to access files on any browser that passes a feature check. The feature is thoroughly tested using sinon and a new mock library. Task-number: QTBUG-99611 Change-Id: I3dd27a9d21eb143c71ea7db0563f70ac7db3a3ac Reviewed-by: Tor Arne Vestbø Reviewed-by: Morten Johan Sørvig --- tests/manual/wasm/qstdweb/files_main.cpp | 471 +++++++++++++++++++++++++++++++ 1 file changed, 471 insertions(+) create mode 100644 tests/manual/wasm/qstdweb/files_main.cpp (limited to 'tests/manual/wasm/qstdweb/files_main.cpp') diff --git a/tests/manual/wasm/qstdweb/files_main.cpp b/tests/manual/wasm/qstdweb/files_main.cpp new file mode 100644 index 00000000000..4dfae7e13b9 --- /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 WITH Qt-GPL-exception-1.0 + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +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 + T* Own(T* plainPtr) { + m_cleanup.emplace_back([plainPtr]() mutable { + delete plainPtr; + }); + return plainPtr; + } + + val m_window; + val m_testSupport; + + std::vector> m_cleanup; + +private slots: + void selectOneFileWithFileDialog(); + void selectMultipleFilesWithFileDialog(); + void cancelFileDialog(); + void rejectFile(); + void saveFileWithFileDialog(); +}; + +class BarrierCallback { +public: + BarrierCallback(int number, std::function onDone) + : m_remaining(number), m_onDone(std::move(onDone)) {} + + void operator()() { + if (!--m_remaining) { + m_onDone(); + } + } + +private: + int m_remaining; + std::function m_onDone; +}; + + +template +std::string argToString(std::add_lvalue_reference_t> arg) { + return std::to_string(arg); +} + +template <> +std::string argToString(const bool& value) { + return value ? "true" : "false"; +} + +template <> +std::string argToString(const std::string& arg) { + return arg; +} + +template <> +std::string argToString(const std::string& arg) { + return arg; +} + +template +struct Matcher { + virtual ~Matcher() = default; + + virtual bool matches(std::string* explanation, const Type& actual) const = 0; +}; + +template +struct AnyMatcher : public Matcher { + bool matches(std::string* explanation, const Type& actual) const final { + Q_UNUSED(explanation); + Q_UNUSED(actual); + return true; + } + + Type m_value; +}; + +template +struct EqualsMatcher : public Matcher { + EqualsMatcher(Type value) : m_value(std::forward(value)) {} + + bool matches(std::string* explanation, const Type& actual) const final { + const bool ret = actual == m_value; + if (!ret) + *explanation += argToString(actual) + " != " + argToString(m_value); + return actual == m_value; + } + + // It is crucial to hold a copy, otherwise we lose const refs. + std::remove_reference_t m_value; +}; + +template +std::unique_ptr> equals(Type value) { + return std::make_unique>(value); +} + +template +std::unique_ptr> any(Type value) { + return std::make_unique>(value); +} + +template +struct Expectation { + std::tuple>...> m_argMatchers; + int m_callCount = 0; + int m_expectedCalls = 1; + + template + bool match(std::string* explanation, const std::tuple& tuple, std::index_sequence) const { + return ( ... && (std::get(m_argMatchers)->matches(explanation, std::get(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>>()); + } +}; + +template +struct Behavior { + std::function m_callback; + + void call(std::function callback) { + m_callback = std::move(callback); + } +}; + +template +std::string argsToString(Args... args) { + return (... + (", " + argToString(args))); +} + +template<> +std::string argsToString<>() { + return ""; +} + +template +struct ExpectationToBehaviorMapping { + Expectation expectation; + Behavior behavior; +}; + +template +class MockCallback { +public: + std::function get() { + return [this](Args... result) -> R { + return processCall(std::forward(result)...); + }; + } + + Behavior& expectCallWith(std::unique_ptr>... matcherArgs) { + auto matchers = std::make_tuple(std::move(matcherArgs)...); + m_behaviorByExpectation.push_back({Expectation {std::move(matchers)}, Behavior {}}); + return m_behaviorByExpectation.back().behavior; + } + + Behavior& expectRepeatedCallWith(int times, std::unique_ptr>... matcherArgs) { + auto matchers = std::make_tuple(std::move(matcherArgs)...); + m_behaviorByExpectation.push_back({Expectation {std::move(matchers), 0, times}, Behavior {}}); + 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& behavior) { + return behavior.expectation.matches(&triedExpectations, std::forward(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> 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()); + fileSelectedCallback->expectCallWith(equals(true)).call([](bool) mutable {}); + + auto* fileBuffer = Own(new QByteArray()); + + auto* acceptFileCallback = Own(new MockCallback()); + acceptFileCallback->expectCallWith(equals(testFileContent.size()), equals("file1.jpg")) + .call([fileBuffer](uint64_t, std::string) mutable -> char* { + fileBuffer->resize(testFileContent.size()); + return fileBuffer->data(); + }); + + auto* fileDataReadyCallback = Own(new MockCallback()); + fileDataReadyCallback->expectCallWith().call([fileBuffer]() mutable { + QWASMCOMPARE(fileBuffer->data(), std::string(testFileContent)); + QWASMSUCCESS(); + }); + + QWasmLocalFileAccess::openFile( + {QStringLiteral("*")}, fileSelectedCallback->get(), acceptFileCallback->get(), fileDataReadyCallback->get()); +} + +void FilesTest::selectMultipleFilesWithFileDialog() +{ + static constexpr std::array 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()); + fileSelectedCallback->expectCallWith(equals(3)).call([](int) mutable {}); + + auto fileBuffer = std::make_shared(); + + auto* acceptFileCallback = Own(new MockCallback()); + acceptFileCallback->expectCallWith(equals(testFileContent[0].size()), equals("file1.jpg")) + .call([fileBuffer](uint64_t, std::string) mutable -> char* { + fileBuffer->resize(testFileContent[0].size()); + return fileBuffer->data(); + }); + acceptFileCallback->expectCallWith(equals(testFileContent[1].size()), equals("file2.jpg")) + .call([fileBuffer](uint64_t, std::string) mutable -> char* { + fileBuffer->resize(testFileContent[1].size()); + return fileBuffer->data(); + }); + acceptFileCallback->expectCallWith(equals(testFileContent[2].size()), equals("file3.jpg")) + .call([fileBuffer](uint64_t, std::string) mutable -> char* { + fileBuffer->resize(testFileContent[2].size()); + return fileBuffer->data(); + }); + + auto* fileDataReadyCallback = Own(new MockCallback()); + fileDataReadyCallback->expectRepeatedCallWith(3).call([fileBuffer]() mutable { + static int callCount = 0; + QWASMCOMPARE(fileBuffer->data(), std::string(testFileContent[callCount])); + + callCount++; + if (callCount == 3) { + QWASMSUCCESS(); + } + }); + + QWasmLocalFileAccess::openFiles( + {QStringLiteral("*")}, 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()); + fileSelectedCallback->expectCallWith(equals(false)).call([](bool) mutable { + QWASMSUCCESS(); + }); + + auto* acceptFileCallback = Own(new MockCallback()); + auto* fileDataReadyCallback = Own(new MockCallback()); + + QWasmLocalFileAccess::openFile( + {QStringLiteral("*")}, 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()); + fileSelectedCallback->expectCallWith(equals(true)).call([](bool) mutable {}); + + auto* fileDataReadyCallback = Own(new MockCallback()); + + auto* acceptFileCallback = Own(new MockCallback()); + acceptFileCallback->expectCallWith(equals(std::string_view(testFileContent).size()), equals("dontwant.dat")) + .call([](uint64_t, const std::string) { + QTimer::singleShot(0, []() { + // No calls to fileDataReadyCallback + QWASMSUCCESS(); + }); + return nullptr; + }); + + QWasmLocalFileAccess::openFile( + {QStringLiteral("*")}, 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(); + QtWasmTest::initTestCase(argc, argv, testObject); + return 0; +} + +#include "files_main.moc" -- cgit v1.2.3