diff --git a/tools/chatgui/CMakeLists.txt b/tools/chatgui/CMakeLists.txt index ae1b0be5..945a8712 100644 --- a/tools/chatgui/CMakeLists.txt +++ b/tools/chatgui/CMakeLists.txt @@ -270,6 +270,15 @@ endif() enable_testing() +add_executable(test_file_drop tests/test_file_drop.cpp src/inputbar.cpp src/audiorecorder.cpp src/audio_device.cpp + src/voicemessageencoder.cpp src/emojipanel.cpp src/emojitabbar.cpp src/emoji.cpp src/animtimer.cpp src/lottieicon.cpp + src/voice_audio_io.cpp transport/miniaudio_impl.c transport/audio_diagnostics.cpp) +target_include_directories(test_file_drop PRIVATE ${UTUN_INCLUDE_DIRS}) +target_link_libraries(test_file_drop PRIVATE ${QT_LIBS} ${QT_TEST_LIB} rlottie::rlottie ZLIB::ZLIB + utun_voice utun pthread ${CMAKE_DL_LIBS}) +add_test(NAME test_file_drop COMMAND test_file_drop) +set_tests_properties(test_file_drop PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen" TIMEOUT 30) + add_executable(test_member_files_browser tests/test_member_files_browser.cpp src/member_files/browser.cpp db/db_manager.cpp) target_link_libraries(test_member_files_browser PRIVATE ${QT_LIBS} ${QT_TEST_LIB} utun pthread) add_test(NAME test_member_files_browser COMMAND test_member_files_browser) diff --git a/tools/chatgui/tests/test_file_drop.cpp b/tools/chatgui/tests/test_file_drop.cpp new file mode 100644 index 00000000..2e264533 --- /dev/null +++ b/tools/chatgui/tests/test_file_drop.cpp @@ -0,0 +1,196 @@ +/* Реальные Qt-события на viewport: drop только готовит вложение, отправку инициирует пользователь. */ +#include "../src/inputbar.h" +#include "../../../lib/debug_config.h" +extern "C" { +#include "media_async/attachment_send.h" +#include "mem.h" +} +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static QString makeFile(const QTemporaryDir& dir, const QString& name, const QByteArray& contents = "test") { + const QString path = dir.filePath(name); + QFile file(path); + if (!file.open(QIODevice::WriteOnly) || file.write(contents) != contents.size()) return {}; + return path; +} + +static bool dropUrls(QWidget* target, const QList& urls, bool process = true) { + QMimeData mime; + mime.setUrls(urls); + QDragEnterEvent enter(QPoint(5, 5), Qt::CopyAction | Qt::MoveAction, &mime, Qt::LeftButton, Qt::NoModifier); + QApplication::sendEvent(target, &enter); + if (!enter.isAccepted()) return false; + QDragMoveEvent move(QPoint(5, 5), Qt::CopyAction | Qt::MoveAction, &mime, Qt::LeftButton, Qt::NoModifier); + QApplication::sendEvent(target, &move); + QDropEvent drop(QPointF(5, 5), Qt::CopyAction | Qt::MoveAction, &mime, Qt::LeftButton, Qt::NoModifier); + QApplication::sendEvent(target, &drop); + if (process) QCoreApplication::processEvents(); + return drop.isAccepted() && drop.dropAction() == Qt::CopyAction; +} + +class FileDropTest : public QObject { + Q_OBJECT +private slots: + void initTestCase() { debug_config_init(); debug_set_category_level(DEBUG_CATEGORY_GENERAL, DEBUG_LEVEL_DEBUG); } + + void previewBeforeExplicitSend_data() { + QTest::addColumn("history"); + QTest::addColumn("dm"); + QTest::newRow("group-input") << false << false; + QTest::newRow("group-history") << true << false; + QTest::newRow("dm-input") << false << true; + QTest::newRow("dm-history") << true << true; + } + void previewBeforeExplicitSend() { + QFETCH(bool, history); QFETCH(bool, dm); + QTemporaryDir dir; QVERIFY(dir.isValid()); + const QString path = makeFile(dir, QString::fromUtf8("файл # 100%.txt")); QVERIFY(!path.isEmpty()); + InputBar bar(nullptr, nullptr); bar.resize(600, 150); bar.show(); + const QString target = dm ? "dm:0123456789abcdef" : "7206723622466219923"; + bar.setRecordingContext(dir.filePath("media"), target); bar.setDmMode(dm); + auto* editor = bar.findChild(); QVERIFY(editor); + editor->setPlainText("draft"); + QListView view; view.resize(600, 400); view.show(); bar.installFileDropTarget(view.viewport()); + QSignalSpy files(&bar, &InputBar::sendFileAttachment); + QSignalSpy texts(&bar, &InputBar::sendMessage); + QVERIFY(dropUrls(history ? view.viewport() : editor->viewport(), {QUrl::fromLocalFile(path)})); + QCOMPARE(files.count(), 0); QCOMPARE(texts.count(), 0); QCOMPARE(editor->toPlainText(), QString("draft")); + QTest::keyClick(editor, Qt::Key_Return); + QCOMPARE(files.count(), 1); QCOMPARE(texts.count(), 1); + QCOMPARE(files.at(0).at(0).toString(), target); QCOMPARE(files.at(0).at(1).toString(), path); + QCOMPARE(files.at(0).at(3).toString(), QFileInfo(path).fileName()); + QCOMPARE(texts.at(0).at(0).toString(), QString("draft")); + QVERIFY(QFile::exists(path)); // CopyAction не удаляет исходный файл. + } + + void rejectInvalidDropsWithoutReplacingTheDraft_data() { + QTest::addColumn("kind"); + for (const char* kind : {"multiple", "remote", "directory", "missing", "empty"}) QTest::newRow(kind) << QString(kind); + } + void rejectInvalidDropsWithoutReplacingTheDraft() { + QFETCH(QString, kind); + QTemporaryDir dir; QVERIFY(dir.isValid()); + const QString first = makeFile(dir, "first.txt"); QVERIFY(!first.isEmpty()); + InputBar bar(nullptr, nullptr); bar.show(); bar.setRecordingContext(dir.filePath("media"), "123"); + auto* editor = bar.findChild(); QVERIFY(editor); + QVERIFY(dropUrls(editor->viewport(), {QUrl::fromLocalFile(first)})); + editor->setPlainText("draft"); + QSignalSpy rejected(&bar, &InputBar::attachmentRejected); + QSignalSpy files(&bar, &InputBar::sendFileAttachment); + QList urls; + if (kind == "multiple") urls = {QUrl::fromLocalFile(first), QUrl::fromLocalFile(first)}; + else if (kind == "remote") urls = {QUrl("https://example.invalid/file.txt")}; + else if (kind == "directory") urls = {QUrl::fromLocalFile(dir.path())}; + else if (kind == "missing") urls = {QUrl::fromLocalFile(dir.filePath("missing.txt"))}; + else urls = {QUrl::fromLocalFile(makeFile(dir, "empty.txt", {}))}; + dropUrls(editor->viewport(), urls); + QCOMPARE(rejected.count(), 1); QCOMPARE(editor->toPlainText(), QString("draft")); + QTest::keyClick(editor, Qt::Key_Return); + QCOMPARE(files.count(), 1); QCOMPARE(files.at(0).at(1).toString(), first); + } + + void queuedDropCannotFollowAChangedAccountOrChannel_data() { + QTest::addColumn("account"); + QTest::newRow("channel-switch-and-back") << false; + QTest::newRow("account-switch-and-back") << true; + } + void queuedDropCannotFollowAChangedAccountOrChannel() { + QFETCH(bool, account); + QTemporaryDir dir; QVERIFY(dir.isValid()); + const QString path = makeFile(dir, "test.txt"); + InputBar bar(nullptr, nullptr); bar.show(); bar.setRecordingContext(dir.filePath("media"), "123"); + auto* editor = bar.findChild(); QVERIFY(editor); + QSignalSpy rejected(&bar, &InputBar::attachmentRejected); + QSignalSpy files(&bar, &InputBar::sendFileAttachment); + QVERIFY(dropUrls(editor->viewport(), {QUrl::fromLocalFile(path)}, false)); + bar.setRecordingContext(dir.filePath(account ? "other-account" : "media"), account ? "123" : "456"); + bar.setRecordingContext(dir.filePath("media"), "123"); + QCoreApplication::processEvents(); + QCOMPARE(rejected.count(), 1); + QTest::keyClick(editor, Qt::Key_Return); QCOMPARE(files.count(), 0); + } + + void pendingFileClearedWhenChatChanges() { + QTemporaryDir dir; QVERIFY(dir.isValid()); + InputBar bar(nullptr, nullptr); bar.show(); bar.setRecordingContext(dir.filePath("media"), "123"); + auto* editor = bar.findChild(); QVERIFY(editor); + QVERIFY(dropUrls(editor->viewport(), {QUrl::fromLocalFile(makeFile(dir, "test.txt"))})); + bar.setRecordingContext(dir.filePath("media"), "456"); + QSignalSpy files(&bar, &InputBar::sendFileAttachment); + QTest::keyClick(editor, Qt::Key_Return); QCOMPARE(files.count(), 0); + } + + void disabledOrUnselectedChatDoesNotAcceptFiles() { + QTemporaryDir dir; QVERIFY(dir.isValid()); + InputBar bar(nullptr, nullptr); bar.show(); + auto* editor = bar.findChild(); QVERIFY(editor); + const QList urls{QUrl::fromLocalFile(makeFile(dir, "test.txt"))}; + QVERIFY(!dropUrls(editor->viewport(), urls)); + bar.setRecordingContext(dir.filePath("media"), "123"); bar.setMediaEnabled(false); + QVERIFY(!dropUrls(editor->viewport(), urls)); + QCOMPARE(editor->toPlainText(), QString()); + } + + void imageKeepsTheExistingGroupAndPmBehaviour_data() { + QTest::addColumn("dm"); + QTest::newRow("group-image") << false; QTest::newRow("dm-file") << true; + } + void imageKeepsTheExistingGroupAndPmBehaviour() { + QFETCH(bool, dm); + QTemporaryDir dir; QVERIFY(dir.isValid()); + InputBar bar(nullptr, nullptr); bar.show(); + bar.setRecordingContext(dir.filePath("media"), dm ? "dm:abcd" : "123"); bar.setDmMode(dm); + auto* editor = bar.findChild(); QVERIFY(editor); + QSignalSpy files(&bar, &InputBar::sendFileAttachment), images(&bar, &InputBar::sendImageAttachment); + QVERIFY(dropUrls(editor->viewport(), {QUrl::fromLocalFile(makeFile(dir, "фото.PNG"))})); + QTest::keyClick(editor, Qt::Key_Return); + QCOMPARE(files.count(), dm ? 1 : 0); QCOMPARE(images.count(), dm ? 0 : 1); + } + + void videoModeSelectionAndCancellation_data() { + QTest::addColumn("mode"); + for (const char* mode : {"file", "video", "cancel", "changed-chat"}) QTest::newRow(mode) << QString(mode); + } + void videoModeSelectionAndCancellation() { + QFETCH(QString, mode); + QTemporaryDir dir; QVERIFY(dir.isValid()); + InputBar bar(nullptr, nullptr); bar.show(); bar.setRecordingContext(dir.filePath("media"), "123"); + auto* editor = bar.findChild(); QVERIFY(editor); + QSignalSpy files(&bar, &InputBar::sendFileAttachment), rejected(&bar, &InputBar::attachmentRejected); + int videos = 0; + connect(&bar, &InputBar::submitAttachment, &bar, [&](const QString& target, attachment_send_req* request) { + QCOMPARE(target, QString("123")); QCOMPARE(request->info.kind, ATTACHMENT_VIDEO); QVERIFY(request->transcode); + ++videos; u_free(request); + }); + QVERIFY(dropUrls(editor->viewport(), {QUrl::fromLocalFile(makeFile(dir, "video.mp4"))}, false)); + QTimer timer; timer.setInterval(1); + connect(&timer, &QTimer::timeout, &bar, [&] { + auto* box = qobject_cast(QApplication::activeModalWidget()); + if (!box) return; + timer.stop(); + if (mode == "cancel") { box->reject(); return; } + if (mode == "changed-chat") bar.setRecordingContext(dir.filePath("media"), "456"); + for (auto* button : box->buttons()) if (button->text() == (mode == "video" ? "As a video" : "As a file")) { + button->click(); return; + } + box->reject(); + }); + timer.start(); QCoreApplication::processEvents(); + QTest::keyClick(editor, Qt::Key_Return); + QCOMPARE(files.count(), mode == "file" ? 1 : 0); QCOMPARE(videos, mode == "video" ? 1 : 0); + QCOMPARE(rejected.count(), mode == "changed-chat" ? 1 : 0); + } +}; + +QTEST_MAIN(FileDropTest) +#include "test_file_drop.moc"