/* Реальные 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 #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 replyDraftAndLateResults() { InputBar bar(nullptr, nullptr); bar.resize(600, 160); bar.show(); bar.setRecordingContext("/tmp", "123"); auto* editor = bar.findChild(); QVERIFY(editor); QSignalSpy sends(&bar, &InputBar::sendMessage); const QByteArray first(32, 'a'), second(32, 'b'); bar.setReply(first, "Alice", "Original"); editor->setPlainText("answer"); QTest::keyClick(editor, Qt::Key_Return); QCOMPARE(sends.size(), 1); QCOMPARE(sends[0][1].toByteArray(), first); const quint64 id = sends[0][2].toULongLong(); QVERIFY(id); QCOMPARE(editor->toPlainText(), QString("answer")); QTest::keyClick(editor, Qt::Key_Return); QCOMPARE(sends.size(), 1); // Не отправлять повторно до результата. bar.submissionResult(id, false); QCOMPARE(editor->toPlainText(), QString("answer")); QTest::keyClick(editor, Qt::Key_Return); QCOMPARE(sends.size(), 2); bar.setReply(second, "Bob", "Another original"); editor->setPlainText("new draft"); bar.submissionResult(sends[1][2].toULongLong(), true); QCOMPARE(editor->toPlainText(), QString("new draft")); QTest::keyClick(editor, Qt::Key_Return); QCOMPARE(sends.size(), 3); QCOMPARE(sends[2][1].toByteArray(), second); bar.submissionResult(sends[2][2].toULongLong(), true); QVERIFY(editor->toPlainText().isEmpty()); editor->setPlainText("ordinary"); QTest::keyClick(editor, Qt::Key_Return); QVERIFY(sends[3][1].toByteArray().isEmpty()); bar.setReply(first, "Alice", "Original"); QTest::keyClick(editor, Qt::Key_Escape); editor->setPlainText("cancelled"); QTest::keyClick(editor, Qt::Key_Return); QVERIFY(sends[4][1].toByteArray().isEmpty()); bar.setReply(first, "Alice", "Original"); bar.setRecordingContext("/tmp", "456"); editor->setPlainText("other channel"); QTest::keyClick(editor, Qt::Key_Return); QVERIFY(sends[5][1].toByteArray().isEmpty()); } void replyAttachmentAndTextHaveIndependentResults() { QTemporaryDir dir; QVERIFY(dir.isValid()); const QString file = makeFile(dir, "answer.txt"); InputBar bar(nullptr, nullptr); bar.resize(600, 180); bar.show(); bar.setRecordingContext(dir.path(), "123"); auto* editor = bar.findChild(); QVERIFY(editor); const QByteArray original(32, 'a'); bar.setReply(original, "Alice", "original"); QVERIFY(dropUrls(editor->viewport(), {QUrl::fromLocalFile(file)})); editor->setPlainText("answer"); QSignalSpy files(&bar, &InputBar::sendFileAttachment), texts(&bar, &InputBar::sendMessage); QTest::keyClick(editor, Qt::Key_Return); QCOMPARE(files.size(), 1); QCOMPARE(texts.size(), 1); QCOMPARE(files[0][4].toByteArray(), original); QCOMPARE(texts[0][1].toByteArray(), original); QVERIFY(files[0][5].toULongLong() != texts[0][2].toULongLong()); bar.submissionResult(texts[0][2].toULongLong(), true); QVERIFY(editor->toPlainText().isEmpty()); bar.submissionResult(files[0][5].toULongLong(), false); QTest::keyClick(editor, Qt::Key_Return); QCOMPARE(files.size(), 2); QCOMPARE(texts.size(), 1); QCOMPARE(files[1][4].toByteArray(), original); bar.submissionResult(files[1][5].toULongLong(), true); editor->setPlainText("next"); QTest::keyClick(editor, Qt::Key_Return); QVERIFY(texts[1][1].toByteArray().isEmpty()); } void replyResultKeepsTextWrittenWhileSending() { InputBar bar(nullptr, nullptr); bar.show(); bar.setRecordingContext("/tmp", "123"); auto* editor = bar.findChild(); QVERIFY(editor); const QByteArray original(32, 'a'); bar.setReply(original, "Alice", "Original"); QSignalSpy sends(&bar, &InputBar::sendMessage); editor->setPlainText("first"); QTest::keyClick(editor, Qt::Key_Return); editor->setPlainText("second"); bar.submissionResult(sends[0][2].toULongLong(), true); QCOMPARE(editor->toPlainText(), QString("second")); QTest::keyClick(editor, Qt::Key_Return); QCOMPARE(sends.size(), 2); QCOMPARE(sends[1][1].toByteArray(), original); } void initTestCase() { debug_config_init(); debug_set_category_level(DEBUG_CATEGORY_GENERAL, DEBUG_LEVEL_DEBUG); QApplication::setAttribute(Qt::AA_DontUseNativeDialogs); } 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 dragHighlightLeavesNoWidgetWhenComposerIsDestroyed() { QTemporaryDir dir; QVERIFY(dir.isValid()); QListView view; view.resize(600, 400); view.show(); auto* bar = new InputBar(nullptr, nullptr); bar->show(); bar->setRecordingContext(dir.filePath("media"), "123"); bar->installFileDropTarget(view.viewport()); QMimeData mime; mime.setUrls({QUrl::fromLocalFile(makeFile(dir, "test.txt"))}); QDragEnterEvent enter(QPoint(5, 5), Qt::CopyAction, &mime, Qt::LeftButton, Qt::NoModifier); QApplication::sendEvent(view.viewport(), &enter); QVERIFY(enter.isAccepted()); QPointer hint = view.viewport()->findChild(); QVERIFY(hint); QVERIFY(!hint->isHidden()); QVERIFY(hint->testAttribute(Qt::WA_TransparentForMouseEvents)); QDragLeaveEvent leave; QApplication::sendEvent(view.viewport(), &leave); QVERIFY(hint->isHidden()); QApplication::sendEvent(view.viewport(), &enter); QVERIFY(!hint->isHidden()); delete bar; QVERIFY(hint.isNull()); } void queuedDropIsCancelledWhenComposerIsDestroyed() { QTemporaryDir dir; QVERIFY(dir.isValid()); auto* bar = new InputBar(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"))}, false)); delete bar; QCoreApplication::processEvents(); QVERIFY(!QDir(dir.filePath("media")).exists()); } void filePickerUsesTheSamePreparationAndContextGuard_data() { QTest::addColumn("mode"); for (const char* mode : {"file", "cancel", "changed-chat"}) QTest::newRow(mode) << QString(mode); } void filePickerUsesTheSamePreparationAndContextGuard() { QFETCH(QString, mode); QTemporaryDir dir; QVERIFY(dir.isValid()); const QString first = makeFile(dir, "first.txt"), second = makeFile(dir, "second.txt"); 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)})); QSignalSpy files(&bar, &InputBar::sendFileAttachment), rejected(&bar, &InputBar::attachmentRejected); QTimer timer; timer.setInterval(1); connect(&timer, &QTimer::timeout, &bar, [&] { auto* dialog = qobject_cast(QApplication::activeModalWidget()); if (!dialog) return; timer.stop(); if (mode == "cancel") { dialog->reject(); return; } if (mode == "changed-chat") bar.setRecordingContext(dir.filePath("media"), "456"); dialog->selectFile(second); QMetaObject::invokeMethod(dialog, "accept", Qt::DirectConnection); }); timer.start(); QVERIFY(QMetaObject::invokeMethod(&bar, "onAttachClicked", Qt::DirectConnection)); QTest::keyClick(editor, Qt::Key_Return); QCOMPARE(files.count(), mode == "changed-chat" ? 0 : 1); QCOMPARE(rejected.count(), mode == "changed-chat" ? 1 : 0); if (!files.isEmpty()) QCOMPARE(files.at(0).at(1).toString(), mode == "cancel" ? first : second); } 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"