From ec856cc73f64ba1abbde4ef07d24b27f6f16dee0 Mon Sep 17 00:00:00 2001 From: evgeny Date: Sun, 4 Oct 2026 19:08:39 +0200 Subject: [PATCH] Verify web link geometry, full addresses, mixed schemes and drag copying --- tools/chatgui/tests/test_invite_links.cpp | 75 +++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/tools/chatgui/tests/test_invite_links.cpp b/tools/chatgui/tests/test_invite_links.cpp index f90a21a1..6953223a 100644 --- a/tools/chatgui/tests/test_invite_links.cpp +++ b/tools/chatgui/tests/test_invite_links.cpp @@ -2,6 +2,7 @@ #include "../src/messagedelegate.h" #include "../src/chatview.h" #include "../src/accountdelegate.h" +#include "../src/message_links.h" #include #include #include @@ -12,6 +13,80 @@ extern "C" { 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 points; + QSet 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); + 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;