You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

309 lines
18 KiB

/* Реальные Qt-события на viewport: drop только готовит вложение, отправку инициирует пользователь. */
#include "../src/inputbar.h"
#include "../../../lib/debug_config.h"
extern "C" {
#include "media_async/attachment_send.h"
#include "mem.h"
}
#include <QtTest>
#include <QTextEdit>
#include <QListView>
#include <QMimeData>
#include <QDropEvent>
#include <QDragEnterEvent>
#include <QDragMoveEvent>
#include <QTemporaryDir>
#include <QFile>
#include <QFileInfo>
#include <QFileDialog>
#include <QLabel>
#include <QTimer>
#include <QUrl>
#include <QMessageBox>
#include <QAbstractButton>
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<QUrl>& 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<QTextEdit*>(); 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<QTextEdit*>(); 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<QTextEdit*>(); 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<bool>("history");
QTest::addColumn<bool>("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<QTextEdit*>(); 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<QString>("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<QTextEdit*>(); QVERIFY(editor);
QVERIFY(dropUrls(editor->viewport(), {QUrl::fromLocalFile(first)}));
editor->setPlainText("draft");
QSignalSpy rejected(&bar, &InputBar::attachmentRejected);
QSignalSpy files(&bar, &InputBar::sendFileAttachment);
QList<QUrl> 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<bool>("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<QTextEdit*>(); 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<QTextEdit*>(); 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<QTextEdit*>(); QVERIFY(editor);
const QList<QUrl> 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<QLabel> hint = view.viewport()->findChild<QLabel*>();
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<QTextEdit*>(); 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<QString>("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<QTextEdit*>(); 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<QFileDialog*>(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<bool>("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<QTextEdit*>(); 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<QString>("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<QTextEdit*>(); 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<QMessageBox*>(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"