summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/corelib/platform/wasm/qstdweb.cpp3
-rw-r--r--src/corelib/platform/wasm/qstdweb_p.h8
-rw-r--r--src/gui/platform/wasm/qwasmlocalfileaccess.cpp216
-rw-r--r--src/gui/platform/wasm/qwasmlocalfileaccess_p.h12
-rw-r--r--src/widgets/dialogs/qfiledialog.cpp10
-rw-r--r--tests/manual/wasm/localfiles/main.cpp176
-rw-r--r--tests/manual/wasm/qstdweb/CMakeLists.txt24
-rw-r--r--tests/manual/wasm/qstdweb/files_auto.html13
-rw-r--r--tests/manual/wasm/qstdweb/files_main.cpp471
-rw-r--r--tests/manual/wasm/qtwasmtestlib/qtwasmtestlib.cpp12
10 files changed, 834 insertions, 111 deletions
diff --git a/src/corelib/platform/wasm/qstdweb.cpp b/src/corelib/platform/wasm/qstdweb.cpp
index 43cd079e9d..486bfaf485 100644
--- a/src/corelib/platform/wasm/qstdweb.cpp
+++ b/src/corelib/platform/wasm/qstdweb.cpp
@@ -451,7 +451,8 @@ File FileList::operator[](int index) const
return item(index);
}
-emscripten::val FileList::val() {
+emscripten::val FileList::val() const
+{
return m_fileList;
}
diff --git a/src/corelib/platform/wasm/qstdweb_p.h b/src/corelib/platform/wasm/qstdweb_p.h
index b4b8948b3a..70f58cb85c 100644
--- a/src/corelib/platform/wasm/qstdweb_p.h
+++ b/src/corelib/platform/wasm/qstdweb_p.h
@@ -91,7 +91,7 @@ namespace qstdweb {
int length() const;
File item(int index) const;
File operator[](int index) const;
- emscripten::val val();
+ emscripten::val val() const;
private:
emscripten::val m_fileList = emscripten::val::undefined();
@@ -183,6 +183,12 @@ namespace qstdweb {
void all(std::vector<emscripten::val> promises, PromiseCallbacks callbacks);
};
+
+ inline emscripten::val window()
+ {
+ static emscripten::val savedWindow = emscripten::val::global("window");
+ return savedWindow;
+ }
}
QT_END_NAMESPACE
diff --git a/src/gui/platform/wasm/qwasmlocalfileaccess.cpp b/src/gui/platform/wasm/qwasmlocalfileaccess.cpp
index 172c8f6814..1b797be9fe 100644
--- a/src/gui/platform/wasm/qwasmlocalfileaccess.cpp
+++ b/src/gui/platform/wasm/qwasmlocalfileaccess.cpp
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#include "qwasmlocalfileaccess_p.h"
+#include "qlocalfileapi_p.h"
#include <private/qstdweb_p.h>
#include <emscripten.h>
#include <emscripten/bind.h>
@@ -11,10 +12,98 @@
QT_BEGIN_NAMESPACE
namespace QWasmLocalFileAccess {
+namespace FileDialog {
+namespace {
+bool hasLocalFilesApi()
+{
+ return !qstdweb::window()["showOpenFilePicker"].isUndefined();
+}
+
+void showOpenViaHTMLPolyfill(const QStringList &accept, FileSelectMode fileSelectMode,
+ qstdweb::PromiseCallbacks onFilesSelected)
+{
+ // Create file input html element which will display a native file dialog
+ // and call back to our onchange handler once the user has selected
+ // one or more files.
+ emscripten::val document = emscripten::val::global("document");
+ emscripten::val input = document.call<emscripten::val>("createElement", std::string("input"));
+ input.set("type", "file");
+ input.set("style", "display:none");
+ // input.set("accept", emscripten::val(accept));
+ Q_UNUSED(accept);
+ input.set("multiple", emscripten::val(fileSelectMode == FileSelectMode::MultipleFiles));
+
+ // Note: there is no event in case the user cancels the file dialog.
+ static std::unique_ptr<qstdweb::EventCallback> changeEvent;
+ auto callback = [=](emscripten::val) { onFilesSelected.thenFunc(input["files"]); };
+ changeEvent = std::make_unique<qstdweb::EventCallback>(input, "change", callback);
+
+ // Activate file input
+ emscripten::val body = document["body"];
+ body.call<void>("appendChild", input);
+ input.call<void>("click");
+ body.call<void>("removeChild", input);
+}
+
+void showOpenViaLocalFileApi(const QStringList &accept, FileSelectMode fileSelectMode,
+ qstdweb::PromiseCallbacks callbacks)
+{
+ using namespace qstdweb;
+
+ auto options = LocalFileApi::makeOpenFileOptions(accept, fileSelectMode == FileSelectMode::MultipleFiles);
+
+ Promise::make(
+ window(), QStringLiteral("showOpenFilePicker"),
+ {
+ .thenFunc = [=](emscripten::val fileHandles) mutable {
+ std::vector<emscripten::val> filePromises;
+ filePromises.reserve(fileHandles["length"].as<int>());
+ for (int i = 0; i < fileHandles["length"].as<int>(); ++i)
+ filePromises.push_back(fileHandles[i].call<emscripten::val>("getFile"));
+ Promise::all(std::move(filePromises), callbacks);
+ },
+ .catchFunc = callbacks.catchFunc,
+ .finallyFunc = callbacks.finallyFunc,
+ }, std::move(options));
+}
+
+void showSaveViaLocalFileApi(const std::string &fileNameHint, qstdweb::PromiseCallbacks callbacks)
+{
+ using namespace qstdweb;
+ using namespace emscripten;
+
+ auto options = LocalFileApi::makeSaveFileOptions(QStringList(), fileNameHint);
+
+ Promise::make(
+ window(), QStringLiteral("showSaveFilePicker"),
+ std::move(callbacks), std::move(options));
+}
+} // namespace
+
+void showOpen(const QStringList &accept, FileSelectMode fileSelectMode,
+ qstdweb::PromiseCallbacks callbacks)
+{
+ hasLocalFilesApi() ?
+ showOpenViaLocalFileApi(accept, fileSelectMode, std::move(callbacks)) :
+ showOpenViaHTMLPolyfill(accept, fileSelectMode, std::move(callbacks));
+}
+
+bool canShowSave()
+{
+ return hasLocalFilesApi();
+}
+
+void showSave(const std::string &fileNameHint, qstdweb::PromiseCallbacks callbacks)
+{
+ Q_ASSERT(canShowSave());
+ showSaveViaLocalFileApi(fileNameHint, std::move(callbacks));
+}
+} // namespace FileDialog
+namespace {
void readFiles(const qstdweb::FileList &fileList,
- const std::function<char *(uint64_t size, const std::string name)> &acceptFile,
- const std::function<void ()> &fileDataReady)
+ const std::function<char *(uint64_t size, const std::string name)> &acceptFile,
+ const std::function<void ()> &fileDataReady)
{
auto readFile = std::make_shared<std::function<void(int)>>();
@@ -25,7 +114,7 @@ void readFiles(const qstdweb::FileList &fileList,
return;
}
- const qstdweb::File file = fileList[fileIndex];
+ const qstdweb::File file = qstdweb::File(fileList[fileIndex]);
// Ask caller if the file should be accepted
char *buffer = acceptFile(file.size(), file.name());
@@ -43,74 +132,103 @@ void readFiles(const qstdweb::FileList &fileList,
(*readFile)(0);
}
+}
-typedef std::function<void (const qstdweb::FileList &fileList)> OpenFileDialogCallback;
-void openFileDialog(const std::string &accept, FileSelectMode fileSelectMode,
- const OpenFileDialogCallback &filesSelected)
+void downloadDataAsFile(const QByteArray &data, const std::string &fileNameHint)
{
- // Create file input html element which will display a native file dialog
- // and call back to our onchange handler once the user has selected
- // one or more files.
+ // Save a file by creating programmatically clicking a download
+ // link to an object url to a Blob containing a copy of the file
+ // content. The copy is made so that the passed in content buffer
+ // can be released as soon as this function returns.
+ qstdweb::Blob contentBlob = qstdweb::Blob::copyFrom(data.constData(), data.size());
emscripten::val document = emscripten::val::global("document");
- emscripten::val input = document.call<emscripten::val>("createElement", std::string("input"));
- input.set("type", "file");
- input.set("style", "display:none");
- input.set("accept", emscripten::val(accept));
- input.set("multiple", emscripten::val(fileSelectMode == MultipleFiles));
-
- // Note: there is no event in case the user cancels the file dialog.
- static std::unique_ptr<qstdweb::EventCallback> changeEvent;
- auto callback = [=](emscripten::val) { filesSelected(qstdweb::FileList(input["files"])); };
- changeEvent.reset(new qstdweb::EventCallback(input, "change", callback));
+ emscripten::val window = qstdweb::window();
+ emscripten::val contentUrl = window["URL"].call<emscripten::val>("createObjectURL", contentBlob.val());
+ emscripten::val contentLink = document.call<emscripten::val>("createElement", std::string("a"));
+ contentLink.set("href", contentUrl);
+ contentLink.set("download", fileNameHint);
+ contentLink.set("style", "display:none");
- // Activate file input
emscripten::val body = document["body"];
- body.call<void>("appendChild", input);
- input.call<void>("click");
- body.call<void>("removeChild", input);
+ body.call<void>("appendChild", contentLink);
+ contentLink.call<void>("click");
+ body.call<void>("removeChild", contentLink);
+
+ window["URL"].call<emscripten::val>("revokeObjectURL", contentUrl);
}
-void openFiles(const std::string &accept, FileSelectMode fileSelectMode,
+void openFiles(const QStringList &accept, FileSelectMode fileSelectMode,
const std::function<void (int fileCount)> &fileDialogClosed,
- const std::function<char *(uint64_t size, const std::string name)> &acceptFile,
+ const std::function<char *(uint64_t size, const std::string& name)> &acceptFile,
const std::function<void()> &fileDataReady)
{
- openFileDialog(accept, fileSelectMode, [=](const qstdweb::FileList &files) {
- fileDialogClosed(files.length());
- readFiles(files, acceptFile, fileDataReady);
+ FileDialog::showOpen(accept, fileSelectMode, {
+ .thenFunc = [=](emscripten::val result) {
+ auto files = qstdweb::FileList(result);
+ fileDialogClosed(files.length());
+ readFiles(files, acceptFile, fileDataReady);
+ },
+ .catchFunc = [=](emscripten::val) {
+ fileDialogClosed(0);
+ }
});
}
-void openFile(const std::string &accept,
+void openFile(const QStringList &accept,
const std::function<void (bool fileSelected)> &fileDialogClosed,
- const std::function<char *(uint64_t size, const std::string name)> &acceptFile,
+ const std::function<char *(uint64_t size, const std::string& name)> &acceptFile,
const std::function<void()> &fileDataReady)
{
auto fileDialogClosedWithInt = [=](int fileCount) { fileDialogClosed(fileCount != 0); };
openFiles(accept, FileSelectMode::SingleFile, fileDialogClosedWithInt, acceptFile, fileDataReady);
}
-void saveFile(const char *content, size_t size, const std::string &fileNameHint)
+void saveDataToFileInChunks(emscripten::val fileHandle, const QByteArray &data)
{
- // Save a file by creating programmatically clicking a download
- // link to an object url to a Blob containing a copy of the file
- // content. The copy is made so that the passed in content buffer
- // can be released as soon as this function returns.
- qstdweb::Blob contentBlob = qstdweb::Blob::copyFrom(content, size);
- emscripten::val document = emscripten::val::global("document");
- emscripten::val window = emscripten::val::global("window");
- emscripten::val contentUrl = window["URL"].call<emscripten::val>("createObjectURL", contentBlob.val());
- emscripten::val contentLink = document.call<emscripten::val>("createElement", std::string("a"));
- contentLink.set("href", contentUrl);
- contentLink.set("download", fileNameHint);
- contentLink.set("style", "display:none");
-
- emscripten::val body = document["body"];
- body.call<void>("appendChild", contentLink);
- contentLink.call<void>("click");
- body.call<void>("removeChild", contentLink);
+ using namespace emscripten;
+ using namespace qstdweb;
+
+ Promise::make(fileHandle, QStringLiteral("createWritable"), {
+ .thenFunc = [=](val writable) {
+ struct State {
+ size_t written;
+ std::function<void(val result)> continuation;
+ };
+
+ auto state = std::make_shared<State>();
+ state->written = 0u;
+ state->continuation = [=](val) mutable {
+ const size_t remaining = data.size() - state->written;
+ if (remaining == 0) {
+ Promise::make(writable, QStringLiteral("close"), { .thenFunc = [=](val) {} });
+ state.reset();
+ return;
+ }
+ static constexpr size_t desiredChunkSize = 1024u;
+ const auto currentChunkSize = std::min(remaining, desiredChunkSize);
+ Promise::make(writable, QStringLiteral("write"), {
+ .thenFunc = state->continuation,
+ }, val(typed_memory_view(currentChunkSize, data.constData() + state->written)));
+ state->written += currentChunkSize;
+ };
+
+ state->continuation(val::undefined());
+ },
+ });
+}
- window["URL"].call<emscripten::val>("revokeObjectURL", contentUrl);
+void saveFile(const QByteArray &data, const std::string &fileNameHint)
+{
+ if (!FileDialog::canShowSave()) {
+ downloadDataAsFile(data, fileNameHint);
+ return;
+ }
+
+ FileDialog::showSave(fileNameHint, {
+ .thenFunc = [=](emscripten::val result) {
+ saveDataToFileInChunks(result, data);
+ },
+ });
}
} // namespace QWasmLocalFileAccess
diff --git a/src/gui/platform/wasm/qwasmlocalfileaccess_p.h b/src/gui/platform/wasm/qwasmlocalfileaccess_p.h
index eb73463759..040fe3c47a 100644
--- a/src/gui/platform/wasm/qwasmlocalfileaccess_p.h
+++ b/src/gui/platform/wasm/qwasmlocalfileaccess_p.h
@@ -23,19 +23,19 @@ QT_BEGIN_NAMESPACE
namespace QWasmLocalFileAccess {
-enum FileSelectMode { SingleFile, MultipleFiles };
+enum class FileSelectMode { SingleFile, MultipleFiles };
-Q_CORE_EXPORT void openFiles(const std::string &accept, FileSelectMode fileSelectMode,
+Q_CORE_EXPORT void openFiles(const QStringList &accept, FileSelectMode fileSelectMode,
const std::function<void (int fileCount)> &fileDialogClosed,
- const std::function<char *(uint64_t size, const std::string name)> &acceptFile,
+ const std::function<char *(uint64_t size, const std::string& name)> &acceptFile,
const std::function<void()> &fileDataReady);
-Q_CORE_EXPORT void openFile(const std::string &accept,
+Q_CORE_EXPORT void openFile(const QStringList &accept,
const std::function<void (bool fileSelected)> &fileDialogClosed,
- const std::function<char *(uint64_t size, const std::string name)> &acceptFile,
+ const std::function<char *(uint64_t size, const std::string& name)> &acceptFile,
const std::function<void()> &fileDataReady);
-Q_CORE_EXPORT void saveFile(const char *content, size_t size, const std::string &fileNameHint);
+Q_CORE_EXPORT void saveFile(const QByteArray &data, const std::string &fileNameHint);
} // namespace QWasmLocalFileAccess
diff --git a/src/widgets/dialogs/qfiledialog.cpp b/src/widgets/dialogs/qfiledialog.cpp
index 966d86b089..885d56c5bb 100644
--- a/src/widgets/dialogs/qfiledialog.cpp
+++ b/src/widgets/dialogs/qfiledialog.cpp
@@ -2304,13 +2304,7 @@ void QFileDialog::getOpenFileContent(const QString &nameFilter, const std::funct
openFileImpl.reset();
};
- auto qtFilterStringToWebAcceptString = [](const QString &qtString) {
- // The Qt and Web name filter string formats are similar, but
- // not identical.
- return qtString.toStdString(); // ### TODO
- };
-
- QWasmLocalFileAccess::openFile(qtFilterStringToWebAcceptString(nameFilter), fileDialogClosed, acceptFile, fileContentReady);
+ QWasmLocalFileAccess::openFile(qt_make_filter_list(nameFilter), fileDialogClosed, acceptFile, fileContentReady);
};
(*openFileImpl)();
@@ -2359,7 +2353,7 @@ void QFileDialog::getOpenFileContent(const QString &nameFilter, const std::funct
void QFileDialog::saveFileContent(const QByteArray &fileContent, const QString &fileNameHint)
{
#ifdef Q_OS_WASM
- QWasmLocalFileAccess::saveFile(fileContent.constData(), fileContent.size(), fileNameHint.toStdString());
+ QWasmLocalFileAccess::saveFile(fileContent, fileNameHint.toStdString());
#else
QFileDialog *dialog = new QFileDialog();
dialog->setAcceptMode(QFileDialog::AcceptSave);
diff --git a/tests/manual/wasm/localfiles/main.cpp b/tests/manual/wasm/localfiles/main.cpp
index 46e2b058c6..39d9f21901 100644
--- a/tests/manual/wasm/localfiles/main.cpp
+++ b/tests/manual/wasm/localfiles/main.cpp
@@ -2,52 +2,136 @@
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
#include <QtWidgets/QtWidgets>
+#include <emscripten/val.h>
+#include <emscripten.h>
+
+class AppWindow : public QObject
+{
+Q_OBJECT
+public:
+ AppWindow() : m_layout(new QVBoxLayout(&m_loadFileUi)),
+ m_window(emscripten::val::global("window")),
+ m_showOpenFilePickerFunction(m_window["showOpenFilePicker"]),
+ m_showSaveFilePickerFunction(m_window["showSaveFilePicker"])
+ {
+ addWidget<QLabel>("Filename filter");
+
+ const bool localFileApiAvailable =
+ !m_showOpenFilePickerFunction.isUndefined() && !m_showSaveFilePickerFunction.isUndefined();
+
+ m_useLocalFileApisCheckbox = addWidget<QCheckBox>("Use the window.showXFilePicker APIs");
+ m_useLocalFileApisCheckbox->setEnabled(localFileApiAvailable);
+ m_useLocalFileApisCheckbox->setChecked(localFileApiAvailable);
+
+ m_filterEdit = addWidget<QLineEdit>("Images (*.png *.jpg);;PDF (*.pdf);;*.txt");
+
+ auto* loadFile = addWidget<QPushButton>("Load File");
+
+ m_fileInfo = addWidget<QLabel>("Opened file:");
+ m_fileInfo->setTextInteractionFlags(Qt::TextSelectableByMouse);
+
+ m_fileHash = addWidget<QLabel>("Sha256:");
+ m_fileHash->setTextInteractionFlags(Qt::TextSelectableByMouse);
+
+ addWidget<QLabel>("Saved file name");
+ m_savedFileNameEdit = addWidget<QLineEdit>("qttestresult");
+
+ m_saveFile = addWidget<QPushButton>("Save File");
+ m_saveFile->setEnabled(false);
+
+ m_layout->addStretch();
+
+ m_loadFileUi.setLayout(m_layout);
+
+ QObject::connect(m_useLocalFileApisCheckbox, &QCheckBox::toggled, std::bind(&AppWindow::onUseLocalFileApisCheckboxToggled, this));
+
+ QObject::connect(loadFile, &QPushButton::clicked, this, &AppWindow::onLoadClicked);
+
+ QObject::connect(m_saveFile, &QPushButton::clicked, std::bind(&AppWindow::onSaveClicked, this));
+ }
+
+ void show() {
+ m_loadFileUi.show();
+ }
+
+ ~AppWindow() = default;
+
+private Q_SLOTS:
+ void onUseLocalFileApisCheckboxToggled()
+ {
+ m_window.set("showOpenFilePicker",
+ m_useLocalFileApisCheckbox->isChecked() ?
+ m_showOpenFilePickerFunction : emscripten::val::undefined());
+ m_window.set("showSaveFilePicker",
+ m_useLocalFileApisCheckbox->isChecked() ?
+ m_showSaveFilePickerFunction : emscripten::val::undefined());
+ }
+
+ void onFileContentReady(const QString &fileName, const QByteArray &fileContents)
+ {
+ m_fileContent = fileContents;
+ m_fileInfo->setText(QString("Opened file: %1 size: %2").arg(fileName).arg(fileContents.size()));
+ m_saveFile->setEnabled(true);
+
+ QTimer::singleShot(100, this, &AppWindow::computeAndDisplayFileHash); // update UI before computing hash
+ }
+
+ void computeAndDisplayFileHash()
+ {
+ QByteArray hash = QCryptographicHash::hash(m_fileContent, QCryptographicHash::Sha256);
+ m_fileHash->setText(QString("Sha256: %1").arg(QString(hash.toHex())));
+ }
+
+ void onFileSaved(bool success)
+ {
+ m_fileInfo->setText(QString("File save result: %1").arg(success ? "success" : "failed"));
+ }
+
+ void onLoadClicked()
+ {
+ QFileDialog::getOpenFileContent(
+ m_filterEdit->text(),
+ std::bind(&AppWindow::onFileContentReady, this, std::placeholders::_1, std::placeholders::_2));
+ }
+
+ void onSaveClicked()
+ {
+ m_fileInfo->setText("Saving file... (no result information with current API)");
+ QFileDialog::saveFileContent(m_fileContent, m_savedFileNameEdit->text());
+ }
+
+private:
+ template <class T, class... Args>
+ T* addWidget(Args... args)
+ {
+ T* widget = new T(std::forward<Args>(args)..., &m_loadFileUi);
+ m_layout->addWidget(widget);
+ return widget;
+ }
+
+ QWidget m_loadFileUi;
+
+ QCheckBox* m_useLocalFileApisCheckbox;
+ QLineEdit* m_filterEdit;
+ QVBoxLayout *m_layout;
+ QLabel* m_fileInfo;
+ QLabel* m_fileHash;
+ QLineEdit* m_savedFileNameEdit;
+ QPushButton* m_saveFile;
+
+ emscripten::val m_window;
+ emscripten::val m_showOpenFilePickerFunction;
+ emscripten::val m_showSaveFilePickerFunction;
+
+ QByteArray m_fileContent;
+};
+
int main(int argc, char **argv)
{
- QApplication app(argc, argv);
-
- QByteArray content;
-
- QWidget loadFileUi;
- QVBoxLayout *layout = new QVBoxLayout();
- QPushButton *loadFile = new QPushButton("Load File");
- QLabel *fileInfo = new QLabel("Opened file:");
- fileInfo->setTextInteractionFlags(Qt::TextSelectableByMouse);
- QLabel *fileHash = new QLabel("Sha256:");
- fileHash->setTextInteractionFlags(Qt::TextSelectableByMouse);
- QPushButton *saveFile = new QPushButton("Save File");
- saveFile->setEnabled(false);
-
- auto onFileReady = [=, &content](const QString &fileName, const QByteArray &fileContents) {
- content = fileContents;
- fileInfo->setText(QString("Opened file: %1 size: %2").arg(fileName).arg(fileContents.size()));
- saveFile->setEnabled(true);
-
- auto computeDisplayFileHash = [=](){
- QByteArray hash = QCryptographicHash::hash(fileContents, QCryptographicHash::Sha256);
- fileHash->setText(QString("Sha256: %1").arg(QString(hash.toHex())));
- };
-
- QTimer::singleShot(100, computeDisplayFileHash); // update UI before computing hash
- };
- auto onLoadClicked = [=](){
- QFileDialog::getOpenFileContent("*.*", onFileReady);
- };
- QObject::connect(loadFile, &QPushButton::clicked, onLoadClicked);
-
- auto onSaveClicked = [=, &content]() {
- QFileDialog::saveFileContent(content, "qtsavefiletest.dat");
- };
- QObject::connect(saveFile, &QPushButton::clicked, onSaveClicked);
-
- layout->addWidget(loadFile);
- layout->addWidget(fileInfo);
- layout->addWidget(fileHash);
- layout->addWidget(saveFile);
- layout->addStretch();
-
- loadFileUi.setLayout(layout);
- loadFileUi.show();
-
- return app.exec();
+ QApplication application(argc, argv);
+ AppWindow window;
+ window.show();
+ return application.exec();
}
+
+#include "main.moc"
diff --git a/tests/manual/wasm/qstdweb/CMakeLists.txt b/tests/manual/wasm/qstdweb/CMakeLists.txt
index ea034fad67..093fe54663 100644
--- a/tests/manual/wasm/qstdweb/CMakeLists.txt
+++ b/tests/manual/wasm/qstdweb/CMakeLists.txt
@@ -20,3 +20,27 @@ add_custom_command(
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
+ PUBLIC_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)
diff --git a/tests/manual/wasm/qstdweb/files_auto.html b/tests/manual/wasm/qstdweb/files_auto.html
new file mode 100644
index 0000000000..71e8088dfb
--- /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(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..4dfae7e13b
--- /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 <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"
diff --git a/tests/manual/wasm/qtwasmtestlib/qtwasmtestlib.cpp b/tests/manual/wasm/qtwasmtestlib/qtwasmtestlib.cpp
index c70c390249..7d1db44cb0 100644
--- a/tests/manual/wasm/qtwasmtestlib/qtwasmtestlib.cpp
+++ b/tests/manual/wasm/qtwasmtestlib/qtwasmtestlib.cpp
@@ -101,10 +101,22 @@ void runTestFunction(std::string name)
QMetaObject::invokeMethod(g_testObject, name.c_str());
}
+void failTest(std::string message)
+{
+ completeTestFunction(QtWasmTest::Fail, std::move(message));
+}
+
+void passTest()
+{
+ completeTestFunction(QtWasmTest::Pass, "");
+}
+
EMSCRIPTEN_BINDINGS(qtwebtestrunner) {
emscripten::function("cleanupTestCase", &cleanupTestCase);
emscripten::function("getTestFunctions", &getTestFunctions);
emscripten::function("runTestFunction", &runTestFunction);
+ emscripten::function("qtWasmFail", &failTest);
+ emscripten::function("qtWasmPass", &passTest);
}
//