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.
483 lines
32 KiB
483 lines
32 KiB
/* Проверка реальных координат перенесённых ссылок и кнопки приглашения в строке участника. */ |
|
#include "../src/messagedelegate.h" |
|
#include "../src/chatview.h" |
|
#include "../src/accountdelegate.h" |
|
#include "../src/memberlistmodel.h" |
|
#include "../src/message_links.h" |
|
#include "../../../lib/debug_config.h" |
|
#include <QtTest> |
|
#include <QStandardItemModel> |
|
#include <QClipboard> |
|
#include <QPainter> |
|
#include <QDir> |
|
#include <QScopedValueRollback> |
|
extern "C" { |
|
#include "chat/invite_link.h" |
|
#include "chat/chat_member.h" |
|
} |
|
|
|
class InviteLinksTest : public QObject { |
|
Q_OBJECT |
|
private slots: |
|
void replyClick_data() { |
|
QTest::addColumn<QString>("type"); QTest::addColumn<bool>("outgoing"); QTest::addColumn<int>("width"); |
|
for (const auto* type : {"text/plain", "audio/opus", "image/png", "video/mp4", "application/octet-stream", "poll", "emoji"}) |
|
for (bool outgoing : {false, true}) for (int width : {280, 600}) |
|
QTest::newRow(qPrintable(QString("%1-%2-%3").arg(type).arg(outgoing).arg(width))) << QString(type) << outgoing << width; |
|
} |
|
|
|
void replyClick() { |
|
QFETCH(QString, type); QFETCH(bool, outgoing); QFETCH(int, width); |
|
QStandardItemModel model; auto* message = new QStandardItem; |
|
message->setData(type == "emoji" ? QString::fromUtf8("😀") : QString("Answer"), MsgTextRole); |
|
message->setData(type == "emoji" ? "text/plain" : type, MsgContentTypeRole); |
|
message->setData(outgoing, MsgIsOutgoingRole); message->setData("1001", MsgChannelIdRole); |
|
message->setData(QString(64, 'a'), MsgMessageKeyRole); message->setData("Alice", MsgAuthorRole); |
|
message->setData("12:34", MsgTimeRole); message->setData("example.txt", MsgFileDisplayNameRole); |
|
message->setData("0.5s", MsgVoiceDurationRole); message->setData(QString(64, 'b'), MsgReplyToRole); |
|
message->setData("Bob", MsgQuoteAuthorRole); message->setData(QString(2000, 'x'), MsgQuoteTextRole); |
|
model.appendRow(message); |
|
ChatView view; MessageDelegate delegate; view.setModel(&model); view.setItemDelegate(&delegate); |
|
view.setSelectionMode(QAbstractItemView::NoSelection); view.resize(width, 500); view.show(); QTest::qWait(10); |
|
const auto index = model.index(0, 0); QStyleOptionViewItem option; option.initFrom(&view); option.widget = &view; |
|
const int before = delegate.sizeHint(option, index).height(); |
|
message->setData(true, MsgHasQuoteRole); view.doItemsLayout(); |
|
option.rect = view.visualRect(index); |
|
QVERIFY(delegate.sizeHint(option, index).height() > before); |
|
QVERIFY(delegate.sizeHint(option, index).height() < before + 120); |
|
QSignalSpy replies(&delegate, &MessageDelegate::replyRequested), polls(&delegate, &MessageDelegate::pollRequested); |
|
int downloads = 0; delegate.setDownloadCallback([&](const QModelIndex&) { downloads++; return true; }); |
|
// Верхняя цитата у медиа/emoji; у текста она ниже имени автора. |
|
const bool text = type == "text/plain" || type == "poll"; |
|
const int y = text ? 8 + (outgoing ? 0 : QFontMetrics(view.font()).height() + 2) + 6 : 6; |
|
const int x = width < 480 ? view.viewport()->width() / 2 : outgoing ? view.viewport()->width() - 170 : 170; |
|
const QPoint quote(x, y); |
|
QTest::mouseClick(view.viewport(), Qt::LeftButton, Qt::NoModifier, quote); |
|
QCOMPARE(replies.size(), 1); QCOMPARE(replies[0][0].toString(), QString(64, 'b')); |
|
QCOMPARE(downloads, 0); QCOMPARE(polls.size(), 0); |
|
QTest::mouseClick(view.viewport(), Qt::RightButton, Qt::NoModifier, quote); QCOMPARE(replies.size(), 1); |
|
QTest::mousePress(view.viewport(), Qt::LeftButton, Qt::NoModifier, quote); |
|
QTest::mouseRelease(view.viewport(), Qt::LeftButton, Qt::NoModifier, quote + QPoint(30, 0)); |
|
QCOMPARE(replies.size(), 1); |
|
QImage image(option.rect.size(), QImage::Format_ARGB32); image.fill(Qt::transparent); |
|
QPainter painter(&image); delegate.paint(&painter, option, index); painter.end(); |
|
} |
|
|
|
void initTestCase() { debug_config_init(); } |
|
void pollLeftClick_data() { |
|
QTest::addColumn<bool>("outgoing"); QTest::addColumn<int>("width"); |
|
for (bool outgoing : {false, true}) for (int width : {280, 600}) |
|
QTest::newRow(qPrintable(QString("%1-%2").arg(outgoing).arg(width))) << outgoing << width; |
|
} |
|
|
|
void pollLeftClick() { |
|
QFETCH(bool, outgoing); QFETCH(int, width); |
|
QScopedValueRollback<debug_level_t> logging(g_debug_config.category_levels[DEBUG_CATEGORY_GENERAL], DEBUG_LEVEL_INFO); |
|
QStandardItemModel model; |
|
auto* message = new QStandardItem; |
|
const QString key(64, 'a'); |
|
message->setData("Which of these desktop options should everyone choose today?\nOption A\nOption B", MsgTextRole); |
|
message->setData("poll", MsgContentTypeRole); message->setData(outgoing, MsgIsOutgoingRole); |
|
message->setData("1001", MsgChannelIdRole); message->setData(key, MsgMessageKeyRole); |
|
message->setData("Alice", MsgAuthorRole); message->setData("12:34", MsgTimeRole); |
|
model.appendRow(message); |
|
ChatView view; MessageDelegate delegate; |
|
view.setModel(&model); view.setItemDelegate(&delegate); view.setFrameShape(QFrame::NoFrame); |
|
view.setSelectionMode(QAbstractItemView::NoSelection); view.setEditTriggers(QAbstractItemView::NoEditTriggers); |
|
QFont font = view.font(); font.setPointSize(10); view.setFont(font); |
|
view.resize(width, 400); view.show(); QTest::qWait(20); |
|
QSignalSpy polls(&delegate, &MessageDelegate::pollRequested); |
|
const QModelIndex index = model.index(0, 0); |
|
const QPoint body(view.viewport()->width() / 2, QFontMetrics(view.font()).height() + 12); |
|
QTest::mouseClick(view.viewport(), Qt::LeftButton, Qt::NoModifier, body); |
|
QCOMPARE(polls.count(), 1); QCOMPARE(polls.first().first().toString(), key); |
|
|
|
QTest::mouseClick(view.viewport(), Qt::RightButton, Qt::NoModifier, body); |
|
QTest::mouseClick(view.viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(0, body.y())); |
|
const QPoint footer(body.x(), view.visualRect(index).bottom() - 10); |
|
QTest::mouseClick(view.viewport(), Qt::LeftButton, Qt::NoModifier, footer); |
|
if (width == 600) { |
|
const QPoint avatar(outgoing ? view.viewport()->width() - 35 : 35, view.visualRect(index).bottom() - 30); |
|
QTest::mouseClick(view.viewport(), Qt::LeftButton, Qt::NoModifier, avatar); |
|
} |
|
QCOMPARE(polls.count(), 1); |
|
|
|
QTest::mousePress(view.viewport(), Qt::LeftButton, Qt::NoModifier, body); |
|
QTest::mouseRelease(view.viewport(), Qt::LeftButton, Qt::NoModifier, body + QPoint(20, 0)); |
|
QCOMPARE(polls.count(), 1); |
|
QTest::mousePress(view.viewport(), Qt::LeftButton, Qt::NoModifier, body); |
|
for (const QPoint point : {body + QPoint(20, 0), body}) { |
|
QMouseEvent drag(QEvent::MouseMove, point, view.viewport()->mapToGlobal(point), Qt::NoButton, Qt::LeftButton, Qt::NoModifier); |
|
QApplication::sendEvent(view.viewport(), &drag); |
|
} |
|
QTest::mouseRelease(view.viewport(), Qt::LeftButton, Qt::NoModifier, body); |
|
QVERIFY(view.hasSelection()); QCOMPARE(polls.count(), 1); // Возврат мыши не превращает выделение в клик. |
|
QTest::mouseClick(view.viewport(), Qt::LeftButton, Qt::NoModifier, body); |
|
QCOMPARE(polls.count(), 2); |
|
message->setData(QString(), MsgMessageKeyRole); |
|
QTest::mouseClick(view.viewport(), Qt::LeftButton, Qt::NoModifier, body); |
|
QCOMPARE(polls.count(), 2); |
|
message->setData(key, MsgMessageKeyRole); message->setData("text/plain", MsgContentTypeRole); |
|
QTest::mouseClick(view.viewport(), Qt::LeftButton, Qt::NoModifier, body); |
|
QCOMPARE(polls.count(), 2); |
|
} |
|
|
|
void pollEmojiQuestion() { |
|
QStandardItemModel model; |
|
auto* message = new QStandardItem; |
|
const QString key(64, 'a'); |
|
message->setData(QString::fromUtf8("😀"), MsgTextRole); message->setData("poll", MsgContentTypeRole); |
|
message->setData("1001", MsgChannelIdRole); message->setData(key, MsgMessageKeyRole); |
|
model.appendRow(message); |
|
ChatView view; MessageDelegate delegate; |
|
view.setModel(&model); view.setItemDelegate(&delegate); view.setFrameShape(QFrame::NoFrame); |
|
view.setSelectionMode(QAbstractItemView::NoSelection); view.resize(300, 250); view.show(); QTest::qWait(20); |
|
QStyleOptionViewItem option; option.initFrom(&view); option.widget = &view; option.rect = view.visualRect(model.index(0, 0)); |
|
const QColor background(1, 2, 3); |
|
QImage image(option.rect.size(), QImage::Format_ARGB32); image.fill(background); |
|
QPainter painter(&image); delegate.paint(&painter, option, model.index(0, 0)); painter.end(); |
|
qint64 xTotal = 0, yTotal = 0, pixels = 0; |
|
for (int y = 0; y < image.height(); ++y) for (int x = 0; x < image.width(); ++x) { |
|
if (image.pixelColor(x, y) == background) continue; |
|
xTotal += x; yTotal += y; ++pixels; |
|
} |
|
QVERIFY(pixels > 0); |
|
QSignalSpy polls(&delegate, &MessageDelegate::pollRequested); |
|
const QPoint question(int(xTotal / pixels), int(yTotal / pixels)); // Нажимаем на реально нарисованный вопрос. |
|
QTest::mouseClick(view.viewport(), Qt::LeftButton, Qt::NoModifier, question); |
|
QCOMPARE(polls.count(), 1); QCOMPARE(polls.first().first().toString(), key); |
|
} |
|
|
|
void reactionFooter_data() { |
|
QTest::addColumn<QString>("type"); QTest::addColumn<bool>("outgoing"); QTest::addColumn<int>("width"); |
|
for (const auto& type : {"text/plain", "audio/opus", "image/png", "video/mp4", "application/octet-stream", "poll"}) |
|
for (bool outgoing : {false, true}) for (int width : {280, 600}) { |
|
const QByteArray name = QString("%1-%2-%3").arg(type).arg(outgoing).arg(width).toUtf8(); |
|
QTest::newRow(name.constData()) << QString(type) << outgoing << width; |
|
} |
|
} |
|
|
|
void reactionFooter() { |
|
QFETCH(QString, type); QFETCH(bool, outgoing); QFETCH(int, width); |
|
QStandardItemModel model; auto* message = new QStandardItem; |
|
message->setData(type == "text/plain" || type == "poll" ? QString("Example message") : QString::fromUtf8("😀"), MsgTextRole); |
|
message->setData(type, MsgContentTypeRole); message->setData(outgoing, MsgIsOutgoingRole); |
|
message->setData("1001", MsgChannelIdRole); message->setData(QString(64, 'a'), MsgMessageKeyRole); |
|
message->setData("Alice", MsgAuthorRole); message->setData("12:34", MsgTimeRole); |
|
message->setData("00:05", MsgVoiceDurationRole); message->setData("example.dat", MsgFileDisplayNameRole); |
|
message->setData(1, MsgIdRole); message->setData(0, MsgFileDownloadStateRole); |
|
message->setData(QVariantList{QVariantMap{{"id", 7}, {"emoji", QString::fromUtf8("👍")}, {"count", 2}, {"mine", true}}}, MsgReactionsRole); |
|
model.appendRow(message); |
|
ChatView view; MessageDelegate delegate; view.setModel(&model); view.setItemDelegate(&delegate); |
|
view.setSelectionMode(QAbstractItemView::NoSelection); view.resize(width, 480); view.show(); QTest::qWait(10); |
|
int downloads = 0; delegate.setDownloadCallback([&](const QModelIndex&) { downloads++; return true; }); |
|
const auto index = model.index(0, 0); QStyleOptionViewItem option; option.initFrom(&view); option.widget = &view; |
|
option.rect = view.visualRect(index); option.rect.setHeight(delegate.sizeHint(option, index).height()); |
|
option.palette.setColor(QPalette::Window, QColor(42, 52, 62)); option.palette.setColor(QPalette::Text, Qt::white); |
|
QSignalSpy requested(&delegate, &MessageDelegate::reactionRequested); |
|
QMap<int, QPoint> points; QRect heartHits; |
|
/* Ищем области по фактическим событиям, не повторяя вычисления layout. */ |
|
for (int y = 1; y < option.rect.height(); y += 4) for (int x = 1; x < view.viewport()->width(); x += 4) { |
|
const QPoint point(x, y); |
|
QMouseEvent press(QEvent::MouseButtonPress, point, view.viewport()->mapToGlobal(point), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier); |
|
QMouseEvent release(QEvent::MouseButtonRelease, point, view.viewport()->mapToGlobal(point), Qt::LeftButton, Qt::NoButton, Qt::NoModifier); |
|
requested.clear(); delegate.editorEvent(&press, &model, option, index); delegate.editorEvent(&release, &model, option, index); |
|
if (!requested.isEmpty()) { |
|
QCOMPARE(requested.count(), 1); const int emoji = requested.first().at(1).toInt(); |
|
if (!points.contains(emoji)) points[emoji] = point; |
|
if (emoji == -1) heartHits = heartHits.isValid() ? heartHits.united(QRect(point, QSize(1, 1))) : QRect(point, QSize(1, 1)); |
|
} |
|
} |
|
QVERIFY(points.contains(-1) && points.contains(7)); downloads = 0; requested.clear(); |
|
QTest::mouseClick(view.viewport(), Qt::LeftButton, Qt::NoModifier, points[-1]); |
|
QCOMPARE(requested.count(), 1); QCOMPARE(requested.last().at(1).toInt(), -1); |
|
QTest::mouseClick(view.viewport(), Qt::LeftButton, Qt::NoModifier, points[7]); |
|
QTest::mouseClick(view.viewport(), Qt::LeftButton, Qt::NoModifier, points[7]); |
|
QCOMPARE(requested.count(), 3); QCOMPARE(requested.last().at(1).toInt(), 7); QCOMPARE(downloads, 0); |
|
QTest::mouseClick(view.viewport(), Qt::RightButton, Qt::NoModifier, points[-1]); QCOMPARE(requested.count(), 3); |
|
QTest::mousePress(view.viewport(), Qt::LeftButton, Qt::NoModifier, points[-1]); |
|
QTest::mouseRelease(view.viewport(), Qt::LeftButton, Qt::NoModifier, points[-1] + QPoint(20, 0)); |
|
QCOMPARE(requested.count(), 3); QCOMPARE(downloads, 0); |
|
|
|
const QColor background(70, 80, 90); QImage image(option.rect.size(), QImage::Format_ARGB32); image.fill(background); |
|
QPainter painter(&image); delegate.paint(&painter, option, index); painter.end(); |
|
const auto bubbleColor = image.pixelColor(heartHits.center() + QPoint(0, 12)); |
|
QVERIFY(bubbleColor != background); /* строка с реакциями находится внутри пузыря */ |
|
QCOMPARE(image.pixelColor(heartHits.center()), bubbleColor); /* внутри сердечко прозрачно */ |
|
bool whiteContour = false; |
|
for (int y = heartHits.top(); y <= heartHits.bottom(); y++) for (int x = heartHits.left(); x <= heartHits.right(); x++) |
|
if (image.pixelColor(x, y) == QColor(Qt::white)) whiteContour = true; |
|
QVERIFY(whiteContour); |
|
if (type == "text/plain" && !outgoing && width == 600) |
|
QVERIFY(image.save(QDir::tempPath() + "/utun2-reaction-heart-preview.png")); |
|
} |
|
|
|
void largeEmojiHasNoReactionFooter() { |
|
QStandardItemModel model; auto* message = new QStandardItem; |
|
message->setData(QString::fromUtf8("😀"), MsgTextRole); message->setData("1001", MsgChannelIdRole); model.appendRow(message); |
|
ChatView view; MessageDelegate delegate; view.setModel(&model); view.setItemDelegate(&delegate); view.resize(600, 240); |
|
QStyleOptionViewItem option; option.initFrom(&view); option.widget = &view; |
|
const auto index = model.index(0, 0); const auto size = delegate.sizeHint(option, index); |
|
message->setData(QString(64, 'a'), MsgMessageKeyRole); QCOMPARE(delegate.sizeHint(option, index), size); |
|
} |
|
|
|
void multipleReactionsExpandBottomRow() { |
|
QStandardItemModel model; auto* message = new QStandardItem; |
|
message->setData("Message", MsgTextRole); message->setData("1001", MsgChannelIdRole); |
|
message->setData(QString(64, 'a'), MsgMessageKeyRole); message->setData("12:34", MsgTimeRole); model.appendRow(message); |
|
ChatView view; MessageDelegate delegate; view.setModel(&model); view.setItemDelegate(&delegate); view.resize(280, 480); |
|
view.show(); QTest::qWait(10); const auto index = model.index(0, 0); const int before = view.visualRect(index).height(); |
|
QVariantList reactions; |
|
for (int i = 0; i < 16; i++) reactions.append(QVariantMap{{"id", i}, {"emoji", QString::fromUtf8("👍")}, {"count", 123}}); |
|
message->setData(reactions, MsgReactionsRole); |
|
QTRY_VERIFY(view.visualRect(index).height() > before); |
|
QStyleOptionViewItem option; option.initFrom(&view); option.widget = &view; |
|
QCOMPARE(view.visualRect(index).height(), delegate.sizeHint(option, index).height()); |
|
message->setData(QVariantList{}, MsgReactionsRole); QTRY_COMPARE(view.visualRect(index).height(), before); |
|
} |
|
|
|
void messageLinkBoundaries() { |
|
QFile fixture(QFINDTESTDATA("message_links_cases.tsv")); |
|
QVERIFY(fixture.open(QIODevice::ReadOnly)); |
|
int count = 0; |
|
for (const auto& line : QString::fromUtf8(fixture.readAll()).split('\n')) { |
|
if (line.isEmpty() || line.startsWith('#')) continue; |
|
auto columns = line.split('\t'); |
|
QString text = columns.takeFirst(); text.replace(QStringLiteral("\\n"), QStringLiteral("\n")); |
|
const auto links = messageLinks(text); |
|
QStringList urls; |
|
int end = 0; |
|
for (const auto& link : links) { |
|
urls.append(link.url); |
|
QVERIFY(link.start >= end); |
|
QCOMPARE(text.mid(link.start, link.length), link.url); |
|
end = link.start + link.length; |
|
} |
|
QCOMPARE(urls, columns); |
|
count++; |
|
} |
|
QVERIFY(count >= 15); |
|
} |
|
|
|
void webLinksUsePaintedCoordinatesAndDragDoesNotOpen() { |
|
const QString first = QStringLiteral("https://пример.рф/путь?") + QString(160, 'a') + QStringLiteral("=1#fragment"); |
|
const QString second = QStringLiteral("http://example.org/a(b)?x=2"); |
|
const QString invite = QStringLiteral("utun://abc+/=="); |
|
const QString text = QStringLiteral("✨ (%1),\nВторая: %2.\nПриглашение: %3").arg(first, second, invite); |
|
QStandardItemModel model; |
|
auto* message = new QStandardItem; |
|
message->setData(text, MsgTextRole); message->setData(QStringLiteral("text/plain"), MsgContentTypeRole); |
|
message->setData(true, MsgIsOutgoingRole); model.appendRow(message); |
|
ChatView view; MessageDelegate delegate; |
|
view.setModel(&model); view.setItemDelegate(&delegate); view.resize(280, 900); |
|
view.setLinkHitTest([&](const QModelIndex& index, const QPoint& point) { |
|
QStyleOptionViewItem option; option.initFrom(&view); option.widget = &view; option.rect = view.visualRect(index); |
|
return delegate.linkAt(option, index, point); |
|
}); |
|
view.show(); QTest::qWait(30); |
|
const QModelIndex index = model.index(0, 0); |
|
QStyleOptionViewItem option; option.initFrom(&view); option.widget = &view; option.rect = view.visualRect(index); |
|
QMap<QString, QPoint> points; |
|
QSet<int> rows; |
|
for (int y = 8; y < option.rect.height(); y += 8) { |
|
for (int x = 8; x < view.viewport()->width(); x += 12) { |
|
const QString hit = delegate.linkAt(option, index, QPoint(x, y)); |
|
if (hit.isEmpty()) continue; |
|
QVERIFY(hit == first || hit == second || hit == invite); |
|
if (!points.contains(hit)) points[hit] = QPoint(x, y); |
|
if (hit == first) rows.insert(y); |
|
} |
|
} |
|
QCOMPARE(points.size(), 3); QVERIFY(rows.size() > 4); |
|
QVERIFY(delegate.linkAt(option, index, QPoint(0, 0)).isEmpty()); |
|
QSignalSpy clicked(&view, &ChatView::linkActivated); |
|
for (const auto& link : {first, second, invite}) { |
|
QTest::mouseClick(view.viewport(), Qt::LeftButton, Qt::NoModifier, points.value(link)); |
|
QCOMPARE(clicked.last().first().toString(), link); |
|
} |
|
QCOMPARE(clicked.count(), 3); |
|
const QPoint start = points.value(first), end = start + QPoint(20, 0); |
|
QTest::mousePress(view.viewport(), Qt::LeftButton, Qt::NoModifier, start); |
|
QMouseEvent drag(QEvent::MouseMove, end, view.viewport()->mapToGlobal(end), Qt::NoButton, Qt::LeftButton, Qt::NoModifier); |
|
QApplication::sendEvent(view.viewport(), &drag); |
|
QTest::mouseRelease(view.viewport(), Qt::LeftButton, Qt::NoModifier, end); |
|
QTest::qWait(20); |
|
QCOMPARE(clicked.count(), 3); |
|
const auto selection = view.textSelection(); |
|
QVERIFY(selection.startChar >= text.indexOf(first) && selection.endChar <= text.indexOf(first) + first.length()); |
|
QVERIFY(selection.endChar > selection.startChar); |
|
QTest::keyClick(&view, Qt::Key_C, Qt::ControlModifier); |
|
QCOMPARE(QApplication::clipboard()->text(), text.mid(selection.startChar, selection.endChar - selection.startChar)); |
|
} |
|
|
|
void compactInvitationUsesFullLinkForClickAndCopy() { |
|
struct InviteData invite = {}; |
|
invite.channelId = 42; invite.join_key = 123; invite.pubkey[0] = 1; invite.addrCount = 8; |
|
for (auto& address : invite.addrs) { address.family = 6; address.proto = INVITE_PROTO_TCP; address.port = 443; } |
|
char buffer[1024]; |
|
QVERIFY(invite_link_encode(&invite, nullptr, buffer, sizeof(buffer)) > 0); |
|
const QString link = QString::fromUtf8(buffer); |
|
const QString text = QStringLiteral("Приглашение в группу «Группа»\n") + link; |
|
for (int width : {240, 480}) { |
|
QStandardItemModel model; |
|
auto* message = new QStandardItem; |
|
message->setData(text, MsgTextRole); message->setData(link, MsgInviteLinkRole); |
|
message->setData(QStringLiteral("text/plain"), MsgContentTypeRole); message->setData(true, MsgIsOutgoingRole); |
|
model.appendRow(message); |
|
ChatView view; MessageDelegate delegate; |
|
view.setModel(&model); view.setItemDelegate(&delegate); view.resize(width, 300); |
|
view.setLinkHitTest([&](const QModelIndex& index, const QPoint& point) { |
|
QStyleOptionViewItem option; option.initFrom(&view); option.widget = &view; option.rect = view.visualRect(index); |
|
return delegate.linkAt(option, index, point); |
|
}); |
|
view.show(); QTest::qWait(30); |
|
const QModelIndex index = model.index(0, 0); |
|
QStyleOptionViewItem option; option.initFrom(&view); option.widget = &view; option.rect = view.visualRect(index); |
|
QPoint firstPoint, lastPoint; |
|
QSet<int> rows; |
|
for (int y = 0; y < option.rect.height(); ++y) { |
|
for (int x = 0; x < view.viewport()->width(); x += 2) { |
|
const QString hit = delegate.linkAt(option, index, QPoint(x, y)); |
|
if (hit.isEmpty()) continue; |
|
QCOMPARE(hit, link); |
|
rows.insert(y); |
|
if (firstPoint.isNull()) firstPoint = QPoint(x, y); |
|
lastPoint = QPoint(x, y); |
|
} |
|
} |
|
QVERIFY(!firstPoint.isNull()); |
|
QVERIFY(rows.size() <= QFontMetrics(option.font).height() + 1); |
|
QVERIFY(option.rect.height() < 160); |
|
QSignalSpy clicked(&view, &ChatView::linkActivated); |
|
QTest::mouseClick(view.viewport(), Qt::LeftButton, Qt::NoModifier, lastPoint); |
|
QCOMPARE(clicked.count(), 1); QCOMPARE(clicked.last().first().toString(), link); |
|
QTest::mousePress(view.viewport(), Qt::LeftButton, Qt::NoModifier, firstPoint); |
|
QMouseEvent drag(QEvent::MouseMove, lastPoint, view.viewport()->mapToGlobal(lastPoint), |
|
Qt::NoButton, Qt::LeftButton, Qt::NoModifier); |
|
QApplication::sendEvent(view.viewport(), &drag); |
|
QTest::mouseRelease(view.viewport(), Qt::LeftButton, Qt::NoModifier, lastPoint); |
|
QTest::qWait(20); |
|
QCOMPARE(clicked.count(), 1); |
|
QTest::keyClick(&view, Qt::Key_C, Qt::ControlModifier); |
|
QCOMPARE(QApplication::clipboard()->text(), link); |
|
QCOMPARE(index.data(MsgTextRole).toString(), text); |
|
} |
|
} |
|
|
|
void wrappedLinksPreserveTheirCompleteText() { |
|
struct InviteData invite = {}; |
|
invite.channelId = 42; invite.join_key = 123; invite.pubkey[0] = 1; invite.addrCount = 8; |
|
for (auto& address : invite.addrs) { address.family = 6; address.proto = INVITE_PROTO_TCP; address.port = 443; } |
|
char buffer[1024]; |
|
QVERIFY(invite_link_encode(&invite, nullptr, buffer, sizeof(buffer)) > 0); |
|
const QString first = QString::fromUtf8(buffer); |
|
invite.join_key++; |
|
QVERIFY(invite_link_encode(&invite, nullptr, buffer, sizeof(buffer)) > 0); |
|
const QString second = QString::fromUtf8(buffer); |
|
const QString text = QStringLiteral("Приглашение\n(%1),\nВторая группа: %2.").arg(first, second); |
|
QStandardItemModel model; |
|
auto* message = new QStandardItem; |
|
message->setData(text, MsgTextRole); message->setData(QStringLiteral("text/plain"), MsgContentTypeRole); |
|
message->setData(true, MsgIsOutgoingRole); |
|
model.appendRow(message); |
|
ChatView view; MessageDelegate delegate; |
|
view.setModel(&model); view.setItemDelegate(&delegate); view.resize(280, 900); |
|
view.setLinkHitTest([&](const QModelIndex& index, const QPoint& point) { |
|
QStyleOptionViewItem option; option.initFrom(&view); option.widget = &view; option.rect = view.visualRect(index); |
|
return delegate.linkAt(option, index, point); |
|
}); |
|
view.show(); QTest::qWait(30); |
|
const QModelIndex index = model.index(0, 0); |
|
QStyleOptionViewItem option; option.initFrom(&view); option.widget = &view; option.rect = view.visualRect(index); |
|
QPoint firstPoint, secondPoint; |
|
QSet<int> firstRows; |
|
for (int y = 10; y < option.rect.height(); y += 8) { |
|
for (int x = 10; x < view.viewport()->width(); x += 16) { |
|
const QString link = delegate.linkAt(option, index, QPoint(x, y)); |
|
if (link == first) { if (firstPoint.isNull()) firstPoint = QPoint(x, y); firstRows.insert(y); } |
|
else if (link == second) secondPoint = QPoint(x, y); |
|
else QVERIFY(link.isEmpty()); |
|
} |
|
} |
|
QVERIFY(firstRows.size() > 4); QVERIFY(!firstPoint.isNull()); QVERIFY(!secondPoint.isNull()); |
|
QVERIFY(delegate.linkAt(option, index, QPoint(0, 0)).isEmpty()); |
|
QSignalSpy clicked(&view, &ChatView::linkActivated); |
|
QTest::mouseClick(view.viewport(), Qt::LeftButton, Qt::NoModifier, firstPoint); |
|
QCOMPARE(clicked.count(), 1); QCOMPARE(clicked.last().first().toString(), first); |
|
QTest::mouseClick(view.viewport(), Qt::LeftButton, Qt::NoModifier, secondPoint); |
|
QCOMPARE(clicked.count(), 2); QCOMPARE(clicked.last().first().toString(), second); |
|
/* Выделение использует те же перенесённые строки, что и отрисовка ссылки. */ |
|
QPoint dragEnd = firstPoint + QPoint(20, 0); |
|
QTest::mousePress(view.viewport(), Qt::LeftButton, Qt::NoModifier, firstPoint); |
|
QMouseEvent drag(QEvent::MouseMove, dragEnd, view.viewport()->mapToGlobal(dragEnd), |
|
Qt::NoButton, Qt::LeftButton, Qt::NoModifier); |
|
QApplication::sendEvent(view.viewport(), &drag); |
|
QTest::mouseRelease(view.viewport(), Qt::LeftButton, Qt::NoModifier, dragEnd); |
|
QTest::qWait(20); |
|
QCOMPARE(clicked.count(), 2); |
|
const auto selection = view.textSelection(); |
|
QVERIFY(selection.startChar >= text.indexOf(first)); |
|
QVERIFY(selection.endChar <= text.indexOf(first) + first.length()); |
|
QVERIFY(selection.endChar > selection.startChar); |
|
QVERIFY(selection.endChar - selection.startChar < 20); |
|
} |
|
|
|
void memberPlusTargetsOtherMembersOnly() { |
|
QStandardItemModel model; |
|
auto* other = new QStandardItem("other"); other->setData(false, Qt::UserRole + 2); model.appendRow(other); |
|
auto* self = new QStandardItem("self"); self->setData(true, Qt::UserRole + 2); model.appendRow(self); |
|
QListView view; AccountDelegate delegate; |
|
view.setModel(&model); view.setItemDelegate(&delegate); view.resize(240, 140); view.show(); |
|
QTest::qWait(30); |
|
QSignalSpy invites(&delegate, &AccountDelegate::inviteRequested); |
|
const QRect row = view.visualRect(model.index(0, 0)); |
|
QTest::mouseClick(view.viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(row.right() - 17, row.center().y())); |
|
QCOMPARE(invites.count(), 1); QCOMPARE(qvariant_cast<QModelIndex>(invites.first().first()).row(), 0); |
|
const QRect own = view.visualRect(model.index(1, 0)); |
|
QTest::mouseClick(view.viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(own.right() - 17, own.center().y())); |
|
QTest::mouseClick(view.viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(60, row.center().y())); |
|
QCOMPARE(invites.count(), 1); |
|
} |
|
|
|
void radioMuteButtonTargetsMemberAndChangesColor() { |
|
QStandardItemModel model; |
|
auto* other = new QStandardItem("other"); other->setData(false, Qt::UserRole + 2); model.appendRow(other); |
|
auto* self = new QStandardItem("self"); self->setData(true, Qt::UserRole + 2); model.appendRow(self); |
|
QListView view; AccountDelegate delegate; |
|
view.setModel(&model); view.setItemDelegate(&delegate); view.resize(280, 140); view.show(); QTest::qWait(30); |
|
QSignalSpy toggles(&delegate, &AccountDelegate::radioMuteRequested); |
|
QSignalSpy invites(&delegate, &AccountDelegate::inviteRequested); |
|
connect(&delegate, &AccountDelegate::radioMuteRequested, &model, [&](const QModelIndex& index) { |
|
model.setData(index, index.data(Qt::UserRole + 3).toInt() ^ CHAT_MEMBER_FLAG_RADIO_MUTED, Qt::UserRole + 3); |
|
}); |
|
const QModelIndex index = model.index(0, 0); |
|
const QRect row = view.visualRect(index); |
|
const QPoint button(row.right() - 50, row.center().y()); |
|
auto colorCount = [&](QRgb color) { |
|
QImage image(view.viewport()->size(), QImage::Format_RGB32); image.fill(Qt::white); |
|
QStyleOptionViewItem option; option.initFrom(&view); option.rect = row; |
|
QPainter painter(&image); delegate.paint(&painter, option, index); painter.end(); |
|
int count = 0; |
|
for (int y = row.top(); y <= row.bottom(); ++y) |
|
for (int x = row.right() - 63; x < row.right() - 35; ++x) count += image.pixel(x, y) == color; |
|
return count; |
|
}; |
|
QVERIFY(colorCount(QColor("#9E9E9E").rgb()) > 0); // Молчащий участник серый; зелёный означает передачу. |
|
model.setData(index, true, MemberListModel::RadioSpeakingRole); |
|
QVERIFY(colorCount(QColor("#4CAF50").rgb()) > 0); |
|
QTest::mouseClick(view.viewport(), Qt::LeftButton, Qt::NoModifier, button); |
|
QCOMPARE(toggles.count(), 1); QCOMPARE(qvariant_cast<QModelIndex>(toggles.first().first()).row(), 0); |
|
QVERIFY(colorCount(QColor("#EF5350").rgb()) > 0); QCOMPARE(colorCount(QColor("#4CAF50").rgb()), 0); |
|
QTest::mouseClick(view.viewport(), Qt::LeftButton, Qt::NoModifier, button); |
|
QCOMPARE(toggles.count(), 2); QVERIFY(colorCount(QColor("#4CAF50").rgb()) > 0); |
|
const QRect own = view.visualRect(model.index(1, 0)); |
|
QTest::mouseClick(view.viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(own.right() - 50, own.center().y())); |
|
QTest::mouseClick(view.viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(65, row.center().y())); |
|
QCOMPARE(toggles.count(), 2); QCOMPARE(invites.count(), 0); |
|
QTest::mousePress(view.viewport(), Qt::LeftButton, Qt::NoModifier, button); |
|
QTest::mouseRelease(view.viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(65, row.center().y())); |
|
QCOMPARE(toggles.count(), 2); |
|
} |
|
}; |
|
QTEST_MAIN(InviteLinksTest) |
|
#include "test_invite_links.moc"
|
|
|