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
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"
|
|
|