/* Реальные Qt-события на viewport: drop только готовит вложение, отправку инициирует пользователь. */ #include "../src/inputbar.h" #include "../src/chatview.h" #include "../src/emojipanel.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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static QSet clipboardPaths() { QSet paths; const QDir directory(QDir::tempPath()); for (const auto& name : directory.entryList({"vibechat-clipboard-*.png"}, QDir::Files)) paths.insert(directory.filePath(name)); return paths; } static void copyImage(const QImage& image, bool encoded = false) { auto* mime = new QMimeData; if (encoded) { QByteArray bytes; QBuffer buffer(&bytes); buffer.open(QIODevice::WriteOnly); if (!image.save(&buffer, "PNG")) qFatal("Cannot create clipboard PNG fixture"); mime->setData("image/png", bytes); } else mime->setImageData(image); mime->setText("image alternate text"); mime->setHtml("image alternate text"); QApplication::clipboard()->setMimeData(mime); } 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 cleanup() { QApplication::clipboard()->clear(); } void clipboardImageUsesPngPipeline_data() { QTest::addColumn("dm"); QTest::addColumn("encrypted"); QTest::addColumn("method"); QTest::newRow("group-ctrl-v") << false << false << QString("ctrl"); QTest::newRow("pm-ctrl-v") << true << false << QString("ctrl"); QTest::newRow("encrypted-shift-insert") << false << true << QString("shift"); QTest::newRow("group-context-menu-png") << false << false << QString("menu"); } void clipboardImageUsesPngPipeline() { QFETCH(bool, dm); QFETCH(bool, encrypted); QFETCH(QString, method); const auto before = clipboardPaths(); QTemporaryDir directory; QVERIFY(directory.isValid()); InputBar bar(nullptr, nullptr); bar.setRecordingContext(directory.path(), dm ? "dm:456" : "123", dm ? 7 : 0); bar.setDmMode(dm); bar.show(); auto* editor = bar.findChild(); QVERIFY(editor); auto* thumbnail = bar.findChild("clipboardImageThumbnail"); QVERIFY(thumbnail); QSignalSpy errors(&bar, &InputBar::attachmentRejected), texts(&bar, &InputBar::sendMessage); attachment_send_req* pending = nullptr; const auto discard = qScopeGuard([&] { attachment_send_discard(pending); }); connect(&bar, &InputBar::submitAttachment, &bar, [&](const QString& target, attachment_send_req* request) { QCOMPARE(target, dm ? QString("dm:456") : QString("123")); QVERIFY(!pending); pending = request; }); QImage original(120, 80, QImage::Format_ARGB32); original.fill(qRgba(17, 61, 95, 127)); editor->setPlainText("existing draft"); copyImage(original, method == "menu"); if (method == "ctrl") QTest::keyClick(editor, Qt::Key_V, Qt::ControlModifier); else if (method == "shift") QTest::keyClick(editor, Qt::Key_Insert, Qt::ShiftModifier); else { std::unique_ptr menu(editor->createStandardContextMenu()); QAction* paste = nullptr; for (auto* action : menu->actions()) if (action->text().remove('&').startsWith("Paste")) paste = action; QVERIFY(paste); QVERIFY(paste->isEnabled()); paste->trigger(); } QCOMPARE(editor->toPlainText(), QString("existing draft")); QVERIFY(!pending); QCOMPARE(texts.size(), 0); QTest::keyClick(editor, Qt::Key_Return); QVERIFY(!pending); QCOMPARE(texts.size(), 0); // PNG ещё кодируется. QApplication::clipboard()->setText("clipboard has changed"); QTRY_VERIFY(!thumbnail->isHidden()); QCOMPARE(errors.size(), 0); QCOMPARE(editor->toPlainText(), QString("existing draft")); const auto drafts = clipboardPaths() - before; QCOMPARE(drafts.size(), 1); const QString draft = *drafts.begin(); QImage decoded(draft); QCOMPARE(decoded.size(), original.size()); QCOMPARE(decoded.pixelColor(0, 0), original.pixelColor(0, 0)); editor->clear(); const QByteArray reply(32, 'r'); if (!dm) bar.setReply(reply, "Member", "Original"); if (encrypted) bar.findChild()->setChecked(true); QTest::keyClick(editor, Qt::Key_Return); QVERIFY(pending); QVERIFY(pending->request_id); QVERIFY(pending->temporary_source); QCOMPARE(pending->info.kind, ATTACHMENT_PNG); QCOMPARE(pending->info.width, 120); QCOMPARE(pending->info.height, 80); QCOMPARE(pending->encrypted, int(encrypted)); QCOMPARE(pending->dm_generation, dm ? UINT64_C(7) : UINT64_C(0)); QVERIFY(QString::fromUtf8(pending->info.name).startsWith("clipboard-")); QVERIFY(QString::fromUtf8(pending->info.name).endsWith(".png")); if (!dm) QCOMPARE(QByteArray(reinterpret_cast(pending->reply_to), 32), reply); const QString source = QString::fromUtf8(pending->source); QVERIFY(source != draft); QCOMPARE(QImage(source).pixelColor(0, 0), original.pixelColor(0, 0)); bar.submissionResult(pending->request_id, true); QVERIFY(thumbnail->isHidden()); QVERIFY(!QFile::exists(draft)); QVERIFY(QFile::exists(source)); // Worker владеет отдельным source после очистки preview. attachment_send_discard(pending); pending = nullptr; QVERIFY(!QFile::exists(source)); QCOMPARE(clipboardPaths(), before); } void clipboardFailureKeepsDraftAndRetries_data() { QTest::addColumn("dm"); QTest::newRow("group") << false; QTest::newRow("pm") << true; } void clipboardFailureKeepsDraftAndRetries() { QFETCH(bool, dm); const auto before = clipboardPaths(); QTemporaryDir directory; InputBar bar(nullptr, nullptr); bar.setRecordingContext(directory.path(), dm ? "dm:456" : "123", dm ? 7 : 0); bar.setDmMode(dm); bar.show(); auto* editor = bar.findChild(); QVERIFY(editor); auto* thumbnail = bar.findChild("clipboardImageThumbnail"); QVERIFY(thumbnail); attachment_send_req* pending = nullptr; const auto discard = qScopeGuard([&] { attachment_send_discard(pending); }); connect(&bar, &InputBar::submitAttachment, &bar, [&](const QString&, attachment_send_req* request) { pending = request; }); QImage image(32, 24, QImage::Format_RGB32); image.fill(Qt::green); copyImage(image); editor->paste(); QTRY_VERIFY(!thumbnail->isHidden()); const auto drafts = clipboardPaths() - before; QCOMPARE(drafts.size(), 1); const QString draft = *drafts.begin(); QTest::keyClick(editor, Qt::Key_Return); QVERIFY(pending); const auto first = pending->request_id; QVERIFY(first); QTest::keyClick(editor, Qt::Key_Return); QCOMPARE(pending->request_id, first); attachment_send_discard(pending); pending = nullptr; QSignalSpy errors(&bar, &InputBar::attachmentRejected); bar.submissionResult(first, false); QCOMPARE(errors.size(), 1); QVERIFY(!thumbnail->isHidden()); QVERIFY(QFile::exists(draft)); QTest::keyClick(editor, Qt::Key_Return); QVERIFY(pending); QVERIFY(pending->request_id != first); bar.submissionResult(pending->request_id, true); QVERIFY(thumbnail->isHidden()); QVERIFY(!QFile::exists(draft)); attachment_send_discard(pending); pending = nullptr; QCOMPARE(clipboardPaths(), before); } void clipboardOldSuccessPreservesNewPaste() { const auto before = clipboardPaths(); QTemporaryDir directory; InputBar bar(nullptr, nullptr); bar.setRecordingContext(directory.path(), "123"); bar.show(); auto* editor = bar.findChild(); auto* thumbnail = bar.findChild("clipboardImageThumbnail"); QVERIFY(editor); QVERIFY(thumbnail); attachment_send_req* pending = nullptr; const auto discard = qScopeGuard([&] { attachment_send_discard(pending); }); connect(&bar, &InputBar::submitAttachment, &bar, [&](const QString&, attachment_send_req* request) { pending = request; }); QImage image(32, 24, QImage::Format_RGB32); image.fill(Qt::red); copyImage(image); editor->paste(); QTRY_VERIFY(!thumbnail->isHidden()); QTest::keyClick(editor, Qt::Key_Return); QVERIFY(pending); const auto oldDrafts = clipboardPaths() - before; QCOMPARE(oldDrafts.size(), 2); image.fill(Qt::blue); copyImage(image); editor->paste(); bar.submissionResult(pending->request_id, true); // Успех A не должен отменить кодирование B. const QString sendSource = QString::fromUtf8(pending->source); auto* status = bar.findChild("mediaPreparationStatus"); QVERIFY(status); QTRY_VERIFY(status->isHidden()); const auto newDrafts = clipboardPaths() - before - QSet{sendSource}; QCOMPARE(newDrafts.size(), 1); QCOMPARE(QImage(*newDrafts.begin()).pixelColor(0, 0), QColor(Qt::blue)); QVERIFY(!thumbnail->isHidden()); QVERIFY(QFile::exists(sendSource)); attachment_send_discard(pending); pending = nullptr; bar.setRecordingContext(directory.path(), "456"); QCOMPARE(clipboardPaths(), before); } void clipboardWorkIsSupersededAndCleaned_data() { QTest::addColumn("action"); for (const char* action : {"replace", "change-chat", "clear", "destroy", "edit", "disable", "file"}) QTest::newRow(action) << QString(action); } void clipboardWorkIsSupersededAndCleaned() { QFETCH(QString, action); const auto before = clipboardPaths(); QTemporaryDir directory; { InputBar bar(nullptr, nullptr); bar.setRecordingContext(directory.path(), "123"); bar.show(); auto* editor = bar.findChild(); QVERIFY(editor); QImage image(120, 80, QImage::Format_RGB32); image.fill(Qt::red); copyImage(image); editor->paste(); if (action != "destroy") { if (action == "change-chat") bar.setRecordingContext(directory.path(), "456"); if (action == "clear") QVERIFY(QMetaObject::invokeMethod(&bar, "clearPendingAttachment", Qt::DirectConnection)); if (action == "edit") { bar.editMessage(QByteArray(32, 'e'), 1, "edit"); bar.cancelEdit(); } if (action == "disable") { bar.setMediaEnabled(false); bar.setMediaEnabled(true); } if (action == "file") QVERIFY(dropUrls(editor->viewport(), {QUrl::fromLocalFile(makeFile(directory, "file.txt"))})); image = QImage(60, 40, QImage::Format_RGB32); image.fill(Qt::blue); copyImage(image); editor->paste(); auto* thumbnail = bar.findChild("clipboardImageThumbnail"); QVERIFY(thumbnail); QTRY_VERIFY(!thumbnail->isHidden()); const auto drafts = clipboardPaths() - before; QCOMPARE(drafts.size(), 1); const QImage decoded(*drafts.begin()); QCOMPARE(decoded.size(), image.size()); QCOMPARE(decoded.pixelColor(0, 0), QColor(Qt::blue)); } } QCOMPARE(clipboardPaths(), before); } void clipboardInvalidImagePreservesDraft_data() { QTest::addColumn("kind"); for (const char* kind : {"corrupt-png", "too-wide", "too-many-pixels"}) QTest::newRow(kind) << QString(kind); } void clipboardInvalidImagePreservesDraft() { QFETCH(QString, kind); const auto before = clipboardPaths(); QTemporaryDir directory; InputBar bar(nullptr, nullptr); bar.setRecordingContext(directory.path(), "123"); bar.show(); auto* editor = bar.findChild(); auto* thumbnail = bar.findChild("clipboardImageThumbnail"); QVERIFY(editor); QVERIFY(thumbnail); QImage original(32, 24, QImage::Format_RGB32); original.fill(Qt::green); copyImage(original); editor->paste(); QTRY_VERIFY(!thumbnail->isHidden()); const auto drafts = clipboardPaths() - before; QCOMPARE(drafts.size(), 1); QSignalSpy errors(&bar, &InputBar::attachmentRejected); if (kind == "corrupt-png") { auto* mime = new QMimeData; mime->setData("image/png", "invalid PNG"); QApplication::clipboard()->setMimeData(mime); } else copyImage(QImage(kind == "too-wide" ? 16385 : 8192, kind == "too-wide" ? 1 : 4097, QImage::Format_Mono)); editor->paste(); QTRY_COMPARE(errors.size(), 1); QCOMPARE(clipboardPaths() - before, drafts); QVERIFY(!thumbnail->isHidden()); QCOMPARE(QImage(*drafts.begin()).pixelColor(0, 0), QColor(Qt::green)); bar.setRecordingContext(directory.path(), "456"); QCOMPARE(clipboardPaths(), before); } void clipboardTextAndUnavailableComposer_data() { QTest::addColumn("mode"); for (const char* mode : {"text", "disabled-media", "no-target", "edit"}) QTest::newRow(mode) << QString(mode); } void clipboardTextAndUnavailableComposer() { QFETCH(QString, mode); const auto before = clipboardPaths(); QTemporaryDir directory; InputBar bar(nullptr, nullptr); bar.setRecordingContext(directory.path(), "123"); auto* editor = bar.findChild(); QVERIFY(editor); QSignalSpy errors(&bar, &InputBar::attachmentRejected); if (mode == "text") { QApplication::clipboard()->setText("обычный текст\nnext line"); editor->paste(); QCOMPARE(editor->toPlainText(), QString("обычный текст\nnext line")); QCOMPARE(errors.size(), 0); } else { if (mode == "disabled-media") bar.setMediaEnabled(false); if (mode == "no-target") bar.setRecordingContext(directory.path(), ""); if (mode == "edit") bar.editMessage(QByteArray(32, 'e'), 1, "edit"); const QString text = editor->toPlainText(); QImage image(32, 24, QImage::Format_RGB32); image.fill(Qt::red); copyImage(image); editor->paste(); QCOMPARE(errors.size(), 1); QCOMPARE(editor->toPlainText(), text); } QCOMPARE(clipboardPaths(), before); } void externalClipboardImage() { const QString platform = QGuiApplication::platformName(); const bool wayland = platform.startsWith("wayland"); if (!wayland && platform != "xcb") QSKIP("Run with QT_QPA_PLATFORM=xcb or wayland to test an external clipboard owner."); const QString program = QStandardPaths::findExecutable(wayland ? "wl-copy" : "xclip"); if (program.isEmpty()) QSKIP("Install wl-copy/xclip to test an external PNG clipboard owner."); const auto before = clipboardPaths(); QTemporaryDir directory; InputBar bar(nullptr, nullptr); bar.setRecordingContext(directory.path(), "123"); bar.show(); bar.activateWindow(); QVERIFY(QTest::qWaitForWindowActive(&bar)); QImage original(120, 80, QImage::Format_ARGB32); original.fill(qRgba(41, 81, 121, 127)); QByteArray bytes; QBuffer buffer(&bytes); QVERIFY(buffer.open(QIODevice::WriteOnly)); QVERIFY(original.save(&buffer, "PNG")); QProcess owner; const auto stop = qScopeGuard([&] { owner.kill(); owner.waitForFinished(1000); }); owner.start(program, wayland ? QStringList{"--foreground", "--type", "image/png"} : QStringList{"-selection", "clipboard", "-target", "image/png", "-in", "-quiet"}); QVERIFY(owner.waitForStarted(1000)); QCOMPARE(owner.write(bytes), qint64(bytes.size())); owner.closeWriteChannel(); QTRY_VERIFY(QApplication::clipboard()->mimeData()->hasFormat("image/png")); auto* editor = bar.findChild(); QVERIFY(editor); editor->paste(); auto* thumbnail = bar.findChild("clipboardImageThumbnail"); QVERIFY(thumbnail); QTRY_VERIFY(!thumbnail->isHidden()); const auto drafts = clipboardPaths() - before; QCOMPARE(drafts.size(), 1); QCOMPARE(QImage(*drafts.begin()).pixelColor(0, 0), original.pixelColor(0, 0)); bar.setRecordingContext(directory.path(), "456"); QCOMPARE(clipboardPaths(), before); } void ordinaryTextWaitsForResultAndCanRetry() { InputBar bar(nullptr, nullptr); bar.setRecordingContext("/tmp", "123"); auto* editor = bar.findChild(); QVERIFY(editor); QSignalSpy sends(&bar, &InputBar::sendMessage), errors(&bar, &InputBar::attachmentRejected); editor->setPlainText("ordinary"); QVERIFY(QMetaObject::invokeMethod(&bar, "onSendClicked", Qt::DirectConnection)); QCOMPARE(sends.size(), 1); const auto request = sends[0][2].toULongLong(); QVERIFY(request); QCOMPARE(editor->toPlainText(), QString("ordinary")); QVERIFY(QMetaObject::invokeMethod(&bar, "onSendClicked", Qt::DirectConnection)); QCOMPARE(sends.size(), 1); bar.submissionResult(request, false); QCOMPARE(errors.size(), 1); QCOMPARE(editor->toPlainText(), QString("ordinary")); QVERIFY(QMetaObject::invokeMethod(&bar, "onSendClicked", Qt::DirectConnection)); QCOMPARE(sends.size(), 2); QVERIFY(sends[1][2].toULongLong() != request); bar.submissionResult(sends[1][2].toULongLong(), true); QVERIFY(editor->toPlainText().isEmpty()); } void ordinaryResultPreservesNewDraftAndContext() { InputBar bar(nullptr, nullptr); bar.setRecordingContext("/tmp", "123"); auto* editor = bar.findChild(); QVERIFY(editor); QSignalSpy sends(&bar, &InputBar::sendMessage); editor->setPlainText("first"); QVERIFY(QMetaObject::invokeMethod(&bar, "onSendClicked", Qt::DirectConnection)); QVERIFY(sends[0][2].toULongLong()); editor->setPlainText("second"); bar.submissionResult(sends[0][2].toULongLong(), true); QCOMPARE(editor->toPlainText(), QString("second")); QVERIFY(QMetaObject::invokeMethod(&bar, "onSendClicked", Qt::DirectConnection)); bar.setRecordingContext("/tmp", "456"); bar.setRecordingContext("/tmp", "123"); editor->setPlainText("returned to channel"); bar.submissionResult(sends[1][2].toULongLong(), true); QCOMPARE(editor->toPlainText(), QString("returned to channel")); } void ordinaryAttachmentWaitsForResultAndCanRetry() { QTemporaryDir dir; QVERIFY(dir.isValid()); const QString file = makeFile(dir, "music.mp3"); InputBar bar(nullptr, nullptr); bar.show(); bar.setRecordingContext(dir.filePath("media"), "123"); auto* editor = bar.findChild(); QVERIFY(editor); auto* clear = bar.findChild("clearAttachmentButton"); QVERIFY(clear); QSignalSpy files(&bar, &InputBar::sendFileAttachment), errors(&bar, &InputBar::attachmentRejected); QVERIFY(dropUrls(editor->viewport(), {QUrl::fromLocalFile(file)})); QTest::keyClick(editor, Qt::Key_Return); QCOMPARE(files.size(), 1); const auto request = files[0][5].toULongLong(); QVERIFY(request); QVERIFY(clear->isVisible()); QTest::keyClick(editor, Qt::Key_Return); QCOMPARE(files.size(), 1); bar.submissionResult(request, false); QCOMPARE(errors.size(), 1); QVERIFY(errors[0][0].toString().contains("music.mp3")); QVERIFY(clear->isVisible()); QVERIFY(QFile::exists(file)); QTest::keyClick(editor, Qt::Key_Return); QCOMPARE(files.size(), 2); const auto retry = files[1][5].toULongLong(); QVERIFY(retry && retry != request); bar.submissionResult(retry, true); QVERIFY(!clear->isVisible()); QTest::keyClick(editor, Qt::Key_Return); QCOMPARE(files.size(), 2); QVERIFY(dropUrls(editor->viewport(), {QUrl::fromLocalFile(file)})); QTest::keyClick(editor, Qt::Key_Return); QCOMPARE(files.size(), 3); const auto old = files[2][5].toULongLong(); QVERIFY(dropUrls(editor->viewport(), {QUrl::fromLocalFile(makeFile(dir, "new.txt"))})); bar.submissionResult(old, true); QVERIFY(clear->isVisible()); QTest::keyClick(editor, Qt::Key_Return); QCOMPARE(files.size(), 4); QCOMPARE(files[3][3].toString(), QString("new.txt")); } void submissionIdsOutliveComposer() { quint64 oldRequest = 0; { InputBar bar(nullptr, nullptr); bar.setRecordingContext("/tmp", "123"); QSignalSpy sends(&bar, &InputBar::sendMessage); bar.findChild()->setPlainText("old"); QVERIFY(QMetaObject::invokeMethod(&bar, "onSendClicked", Qt::DirectConnection)); oldRequest = sends[0][2].toULongLong(); } InputBar bar(nullptr, nullptr); bar.setRecordingContext("/tmp", "123"); auto* editor = bar.findChild(); editor->setPlainText("new"); QSignalSpy sends(&bar, &InputBar::sendMessage); QVERIFY(QMetaObject::invokeMethod(&bar, "onSendClicked", Qt::DirectConnection)); QVERIFY(oldRequest); QVERIFY(sends[0][2].toULongLong() != oldRequest); bar.submissionResult(oldRequest, true); QCOMPARE(editor->toPlainText(), QString("new")); } void attachmentHandlerCanChangeOrDestroyComposer_data() { QTest::addColumn("destroy"); QTest::newRow("change-context") << false; QTest::newRow("destroy") << true; } void attachmentHandlerCanChangeOrDestroyComposer() { QFETCH(bool, destroy); QTemporaryDir dir; QVERIFY(dir.isValid()); const QString file = makeFile(dir, "file.txt"); QPointer bar = new InputBar(nullptr, nullptr); auto cleanup = qScopeGuard([&] { delete bar.data(); }); bar->setRecordingContext(dir.path(), "123"); auto* editor = bar->findChild(); QVERIFY(editor); QVERIFY(dropUrls(editor->viewport(), {QUrl::fromLocalFile(file)})); editor->setPlainText("old text"); QSignalSpy texts(bar.data(), &InputBar::sendMessage); QString target, source; connect(bar.data(), &InputBar::sendFileAttachment, this, [&](const QString&, const QString&, const QString&, const QString&, const QByteArray&, quint64) { if (destroy) delete bar.data(); else { bar->setRecordingContext(dir.path(), "456"); editor->setPlainText("new draft"); } }); connect(bar.data(), &InputBar::sendFileAttachment, this, [&](const QString& capturedTarget, const QString& capturedSource, const QString&, const QString&, const QByteArray&, quint64) { target = capturedTarget; source = capturedSource; }); QVERIFY(QMetaObject::invokeMethod(bar.data(), "onSendClicked", Qt::DirectConnection)); QCOMPARE(texts.size(), 0); if (destroy) QVERIFY(!bar); else { QCOMPARE(target, QString("123")); QCOMPARE(source, file); QCOMPARE(editor->toPlainText(), QString("new draft")); } } void emojiSelectionInsertsCorrectTextAtCursor() { EmojiPanel panel; InputBar input(&panel, nullptr); auto* editor = input.findChild(); QVERIFY(editor); editor->setPlainText("beforeafter"); auto cursor = editor->textCursor(); cursor.setPosition(6); editor->setTextCursor(cursor); QPushButton* button = nullptr; for (auto* candidate : panel.findChildren()) { if (candidate->toolTip() == ":pinched_fingers:") { button = candidate; break; } } QVERIFY(button); QSignalSpy selected(&panel, &EmojiPanel::emojiSelected); button->click(); const QString emoji = QString::fromUtf8("🤌"); QCOMPARE(selected.count(), 1); QCOMPARE(selected.first().first().toString(), emoji); QCOMPARE(editor->toPlainText(), "before" + emoji + "after"); QCOMPARE(editor->textCursor().position(), 6 + emoji.size()); } void deletedPmDropsDraftAndLatePreparation() { QTemporaryDir source; QVERIFY(source.isValid()); const QString path = makeFile(source, "private.txt"); QVERIFY(!path.isEmpty()); InputBar input(nullptr, nullptr); input.setDmMode(true); input.setRecordingContext(source.path(), "dm:42", 1); input.show(); auto* editor = input.findChild(); QVERIFY(editor); editor->setPlainText("old draft"); QVERIFY(dropUrls(editor->viewport(), {QUrl::fromLocalFile(path)})); const QByteArray oldJob(16, 'a'), newJob(16, 'b'); input.updatePreparation("dm:42", oldJob, 0); auto statusVisible = [&input](const QString& text) { for (auto* label : input.findChildren()) if (label->isVisible() && label->text() == text) return true; return false; }; QVERIFY(statusVisible("Preparing media…")); input.forgetDmHistory("dm:42", 1); input.setRecordingContext(source.path(), "dm:42", 2); input.updatePreparation("dm:42", oldJob, 2); QVERIFY(editor->toPlainText().isEmpty()); QVERIFY(QFileInfo::exists(path)); QVERIFY(!statusVisible("Preparing media…")); QVERIFY(!statusVisible("Failed to prepare attachment")); QSignalSpy files(&input, &InputBar::sendFileAttachment); QVERIFY(QMetaObject::invokeMethod(&input, "onSendClicked", Qt::DirectConnection)); QCOMPARE(files.size(), 0); editor->setPlainText("new draft"); input.setEnabled(false); QSignalSpy messages(&input, &InputBar::sendMessage); QVERIFY(QMetaObject::invokeMethod(&input, "onSendClicked", Qt::DirectConnection)); QCOMPARE(messages.size(), 0); QCOMPARE(editor->toPlainText(), QString("new draft")); input.setEnabled(true); input.updatePreparation("dm:42", newJob, 0); QVERIFY(statusVisible("Preparing media…")); input.updatePreparation("dm:42", newJob, 1); QVERIFY(!statusVisible("Preparing media…")); input.setRecordingContext(source.path(), "dm:42", 3); QVERIFY(editor->toPlainText().isEmpty()); // Смена поколения также аннулирует прежний composer. } void historyKeyboardNavigation_data() { QTest::addColumn("key"); QTest::addColumn("focus"); for (int key : {Qt::Key_Up, Qt::Key_Down, Qt::Key_PageUp, Qt::Key_PageDown, Qt::Key_End}) for (int focus = 0; focus < 3; focus++) QTest::newRow(qPrintable(QStringLiteral("key-%1-focus-%2").arg(key).arg(focus))) << key << focus; } void historyKeyboardNavigation() { QFETCH(int, key); QFETCH(int, focus); QMainWindow window; auto* body = new QWidget(&window); auto* layout = new QVBoxLayout(body); auto* view = new ChatView(body); auto* input = new InputBar(nullptr, nullptr, body); layout->addWidget(view, 1); layout->addWidget(input); window.setCentralWidget(body); QStringList rows; for (int i = 0; i < 100; i++) rows.append(QString::number(i)); QStringListModel model(rows); view->setModel(&model); view->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); auto* editor = input->findChild(); QVERIFY(editor); QCOMPARE(input->focusProxy(), editor); editor->installEventFilter(view); window.installEventFilter(view); const QString draft = QStringLiteral("first line\nsecond line\nthird line"); editor->setPlainText(draft); auto cursor = editor->textCursor(); cursor.setPosition(5); editor->setTextCursor(cursor); window.resize(600, 400); window.show(); window.activateWindow(); QWidget* target = focus == 0 ? static_cast(editor) : focus == 1 ? static_cast(&window) : view; if (focus == 0) input->setFocus(); else target->setFocus(); QTRY_COMPARE(QApplication::focusWidget(), target); auto* bar = view->verticalScrollBar(); QTRY_VERIFY(bar->maximum() > bar->pageStep() * 2); bar->setValue(bar->maximum() / 2); const int before = bar->value(); const bool up = key == Qt::Key_Up || key == Qt::Key_PageUp; const int step = key == Qt::Key_Up || key == Qt::Key_Down ? bar->singleStep() : bar->pageStep(); QSignalSpy latest(view, &ChatView::latestRequested); QTest::keyClick(target, static_cast(key)); QCOMPARE(bar->value(), key == Qt::Key_End ? bar->maximum() : before + (up ? -step : step)); QCOMPARE(latest.count(), key == Qt::Key_End ? 1 : 0); QCOMPARE(QApplication::focusWidget(), target); QCOMPARE(editor->textCursor().position(), 5); QCOMPARE(editor->toPlainText(), draft); bar->setValue(up ? bar->minimum() : bar->maximum()); const int boundary = bar->value(); QTest::keyClick(target, static_cast(key)); QCOMPARE(bar->value(), boundary); QCOMPARE(editor->textCursor().position(), 5); if (focus == 0) { bar->setValue(before); QTest::keyClick(editor, Qt::Key_Down, Qt::ShiftModifier); QVERIFY(editor->textCursor().hasSelection()); QCOMPARE(bar->value(), before); QCOMPARE(editor->toPlainText(), draft); QCOMPARE(QApplication::focusWidget(), editor); } } void encryptedAttachmentCapturesTargetAndResetsChoice_data() { QTest::addColumn("picker"); QTest::newRow("drop") << false; QTest::newRow("file-picker") << true; } void encryptedAttachmentCapturesTargetAndResetsChoice() { QFETCH(bool, picker); QTemporaryDir dir; QVERIFY(dir.isValid()); const QString file = makeFile(dir, "private.txt", "private bytes"); InputBar bar(nullptr, nullptr); bar.show(); bar.setRecordingContext(dir.filePath("media"), "123"); auto* editor = bar.findChild(); QVERIFY(editor); auto* encrypt = bar.findChild("encryptAttachmentCheckBox"); QVERIFY(encrypt); auto* clear = bar.findChild("clearAttachmentButton"); QVERIFY(clear); QVERIFY(!encrypt->isVisible()); QVERIFY(!encrypt->isChecked()); QSignalSpy ordinary(&bar, &InputBar::sendFileAttachment); int encrypted = 0; connect(&bar, &InputBar::submitAttachment, &bar, [&](const QString& target, attachment_send_req* request) { QCOMPARE(target, QString("123")); QVERIFY(request->encrypted); QCOMPARE(request->info.kind, ATTACHMENT_FILE); QCOMPARE(QString::fromUtf8(request->source), file); QCOMPARE(QString::fromUtf8(request->info.name), QString("private.txt")); ++encrypted; const auto id = request->request_id; u_free(request); bar.submissionResult(id, true); }); QTimer timer; timer.setInterval(1); if (picker) { connect(&timer, &QTimer::timeout, &bar, [&] { auto* dialog = qobject_cast(QApplication::activeModalWidget()); if (!dialog) return; timer.stop(); dialog->selectFile(file); QMetaObject::invokeMethod(dialog, "accept", Qt::DirectConnection); }); timer.start(); QVERIFY(QMetaObject::invokeMethod(&bar, "onAttachClicked", Qt::DirectConnection)); } else QVERIFY(dropUrls(editor->viewport(), {QUrl::fromLocalFile(file)})); QVERIFY(encrypt->isVisible()); QVERIFY(!encrypt->isChecked()); QTest::mouseClick(encrypt, Qt::LeftButton); QVERIFY(encrypt->isChecked()); QTest::keyClick(editor, Qt::Key_Return); QCOMPARE(encrypted, 1); QCOMPARE(ordinary.size(), 0); QVERIFY(!encrypt->isChecked()); QVERIFY(QFile::exists(file)); QVERIFY(!encrypt->isVisible()); QVERIFY(dropUrls(editor->viewport(), {QUrl::fromLocalFile(file)})); QTest::mouseClick(encrypt, Qt::LeftButton); QVERIFY(encrypt->isChecked()); QTest::mouseClick(clear, Qt::LeftButton); QVERIFY(!encrypt->isVisible()); QVERIFY(!encrypt->isChecked()); QVERIFY(dropUrls(editor->viewport(), {QUrl::fromLocalFile(file)})); QTest::mouseClick(encrypt, Qt::LeftButton); QVERIFY(encrypt->isChecked()); QVERIFY(dropUrls(editor->viewport(), {QUrl::fromLocalFile(dir.filePath("missing.txt"))})); QVERIFY(encrypt->isChecked()); QVERIFY(dropUrls(editor->viewport(), {QUrl::fromLocalFile(makeFile(dir, "second.txt"))})); QVERIFY(encrypt->isVisible()); QVERIFY(!encrypt->isChecked()); QTest::mouseClick(encrypt, Qt::LeftButton); QVERIFY(encrypt->isChecked()); bar.setRecordingContext(dir.filePath("media"), "456"); QVERIFY(!encrypt->isVisible()); QVERIFY(!encrypt->isChecked()); bar.setDmMode(true); QVERIFY(dropUrls(editor->viewport(), {QUrl::fromLocalFile(file)})); QVERIFY(!encrypt->isVisible()); QVERIFY(!encrypt->isChecked()); } void editDraftSaveCancelAndFailure() { InputBar bar(nullptr, nullptr); bar.resize(600, 160); bar.show(); bar.setRecordingContext("/tmp", "123"); auto* editor = bar.findChild(); QVERIFY(editor); const QByteArray id(32, 'm'), reply(32, 'r'); QSignalSpy edits(&bar, &InputBar::saveEdit), sends(&bar, &InputBar::sendMessage), errors(&bar, &InputBar::attachmentRejected); bar.setReply(reply, "Alice", "original"); editor->setPlainText("draft"); bar.editMessage(id, 2, "old"); QCOMPARE(editor->toPlainText(), QString("old")); QTest::keyClick(editor, Qt::Key_Escape); QCOMPARE(editor->toPlainText(), QString("draft")); bar.editMessage(id, 2, "old"); editor->setPlainText("new"); QTest::keyClick(editor, Qt::Key_Return); QCOMPARE(edits.size(), 1); QVERIFY(sends.isEmpty()); QCOMPARE(edits[0][0].toString(), QString("123")); QCOMPARE(edits[0][1].toByteArray(), id); QCOMPARE(edits[0][2].toULongLong(), quint64(2)); QCOMPARE(edits[0][3].toString(), QString("new")); QVERIFY(editor->isReadOnly()); const auto request = edits[0][4].toULongLong(); QVERIFY(request); QTest::keyClick(editor, Qt::Key_Return); QCOMPARE(edits.size(), 1); bar.submissionResult(request, false); QCOMPARE(errors.size(), 1); QVERIFY(!editor->isReadOnly()); QCOMPARE(editor->toPlainText(), QString("new")); QTest::keyClick(editor, Qt::Key_Return); QCOMPARE(edits.size(), 2); bar.submissionResult(edits[1][4].toULongLong(), true); QCOMPARE(editor->toPlainText(), QString("draft")); QTest::keyClick(editor, Qt::Key_Return); QCOMPARE(sends.size(), 1); QCOMPARE(sends[0][1].toByteArray(), reply); bar.submissionResult(sends[0][2].toULongLong(), true); editor->setPlainText("another draft"); bar.editMessage(id, 3, "edit"); QTest::keyClick(editor, Qt::Key_Return); bar.setRecordingContext("/tmp", "456"); editor->setPlainText("new channel draft"); bar.submissionResult(edits.last()[4].toULongLong(), true); QCOMPARE(editor->toPlainText(), QString("new channel draft")); } 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 imageHasTypedMetadataInGroupAndPm_data() { QTest::addColumn("dm"); QTest::addColumn("format"); QTest::addColumn("encrypted"); for (bool dm : {false, true}) for (bool encrypted : {false, true}) for (const QString& format : {QString("PNG"), QString("JPEG")}) { if (dm && encrypted) continue; QTest::newRow(qPrintable(QString("%1-%2-%3").arg(dm ? "pm" : "group", format).arg(encrypted))) << dm << format << encrypted; } } void imageHasTypedMetadataInGroupAndPm() { QFETCH(bool, dm); QFETCH(QString, format); QFETCH(bool, encrypted); QTemporaryDir dir; QVERIFY(dir.isValid()); InputBar bar(nullptr, nullptr); bar.show(); bar.setRecordingContext(dir.filePath("media"), dm ? "dm:456" : "123", dm ? 7 : 0); bar.setDmMode(dm); auto* editor = bar.findChild(); QVERIFY(editor); const QString path = dir.filePath("фото.PNG"); // Фактический JPEG должен победить расширение PNG. QImage image(120, 80, QImage::Format_ARGB32); image.fill(Qt::red); QVERIFY(image.save(path, qPrintable(format))); int sends = 0; connect(&bar, &InputBar::submitAttachment, &bar, [&](const QString& target, attachment_send_req* request) { ++sends; QCOMPARE(target, dm ? QString("dm:456") : QString("123")); QCOMPARE(request->info.kind, format == "PNG" ? ATTACHMENT_PNG : ATTACHMENT_JPEG); QCOMPARE(request->info.width, 120); QCOMPARE(request->info.height, 80); QCOMPARE(request->dm_generation, dm ? UINT64_C(7) : UINT64_C(0)); QCOMPARE(request->encrypted, (int)encrypted); QCOMPARE(QString::fromUtf8(request->source), path); QVERIFY(!request->temporary_source); const auto id = request->request_id; attachment_send_discard(request); bar.submissionResult(id, true); }); QVERIFY(dropUrls(editor->viewport(), {QUrl::fromLocalFile(path)})); if (encrypted) { auto* check = bar.findChild(); QVERIFY(check); check->setChecked(true); } QTest::keyClick(editor, Qt::Key_Return); QCOMPARE(sends, 1); QVERIFY(QFile::exists(path)); } void corruptImageIsRejectedAndPreservesSelection() { QTemporaryDir dir; InputBar bar(nullptr, nullptr); bar.show(); bar.setRecordingContext(dir.path(), "123"); auto* editor = bar.findChild(); QVERIFY(editor); QSignalSpy errors(&bar, &InputBar::attachmentRejected); int sends = 0; connect(&bar, &InputBar::submitAttachment, &bar, [&](const QString&, attachment_send_req* req) { ++sends; attachment_send_discard(req); }); const QString path = makeFile(dir, "broken.jpg"); QVERIFY(dropUrls(editor->viewport(), {QUrl::fromLocalFile(path)})); QTest::keyClick(editor, Qt::Key_Return); QCOMPARE(sends, 0); QVERIFY(!errors.isEmpty()); QVERIFY(QFile::exists(path)); } 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"