221 lines
13 KiB
C++
221 lines
13 KiB
C++
/* Проверка реальных координат перенесённых ссылок и кнопки приглашения в строке участника. */
|
|
#include "../src/messagedelegate.h"
|
|
#include "../src/chatview.h"
|
|
#include "../src/accountdelegate.h"
|
|
#include "../src/message_links.h"
|
|
#include <QtTest>
|
|
#include <QStandardItemModel>
|
|
#include <QClipboard>
|
|
extern "C" {
|
|
#include "chat/invite_link.h"
|
|
}
|
|
|
|
class InviteLinksTest : public QObject {
|
|
Q_OBJECT
|
|
private slots:
|
|
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);
|
|
}
|
|
};
|
|
QTEST_MAIN(InviteLinksTest)
|
|
#include "test_invite_links.moc"
|