mirror of
https://github.com/crystalidea/qt6windows7.git
synced 2024-11-24 04:20:46 +08:00
472 lines
15 KiB
C++
472 lines
15 KiB
C++
|
// 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 <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(
|
||
|
{QStringLiteral("*")}, 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(
|
||
|
{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<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(
|
||
|
{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<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(
|
||
|
{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<FilesTest>();
|
||
|
QtWasmTest::initTestCase<QCoreApplication>(argc, argv, testObject);
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
#include "files_main.moc"
|