// 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"