File indexing completed on 2025-01-12 04:34:37

0001 /*
0002    SPDX-FileCopyrightText: 2021 David Faure <faure@kde.org>
0003 
0004    SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "textselectiontest.h"
0008 #include "delegateutils/textselection.h"
0009 #include "model/messagesmodel.h"
0010 
0011 #include <QSignalSpy>
0012 #include <QTest>
0013 #include <QTextCursor>
0014 #include <QTextDocument>
0015 #include <QTextDocumentFragment>
0016 
0017 QTEST_MAIN(TextSelectionTest)
0018 
0019 TextSelectionTest::TextSelectionTest(QObject *parent)
0020     : QObject(parent)
0021 {
0022 }
0023 
0024 class TestFactory : public DocumentFactoryInterface
0025 {
0026 public:
0027     explicit TestFactory(int numRows)
0028     {
0029         mTextDocs.resize(numRows);
0030     }
0031     /**
0032      * Creates (or retrieves from a cache) the QTextDocument for a given @p index.
0033      * @param width The width for layouting that QTextDocument. -1 if no layouting is desired (e.g. for converting to text or HTML)
0034      * @param widget The view to update when fetching thread context on demand. nullptr if this isn't needed (e.g. from SelectionManager)
0035      * @return the QTextDocument. Ownership remains with the cache, don't delete it.
0036      */
0037     QTextDocument *documentForIndex(const QModelIndex &index) const override
0038     {
0039         const int row = index.row();
0040         if (!mTextDocs[row]) {
0041             const QString text = index.data(MessagesModel::MessageConvertedText).toString();
0042             auto doc = std::make_unique<QTextDocument>();
0043             doc->setHtml(text);
0044             mTextDocs[row] = std::move(doc);
0045         }
0046         return mTextDocs[row].get();
0047     }
0048 
0049 private:
0050     mutable std::vector<std::unique_ptr<QTextDocument>> mTextDocs;
0051 };
0052 
0053 static QStandardItem *newItem(const QString &text)
0054 {
0055     auto item = new QStandardItem;
0056     item->setData(text, MessagesModel::MessageConvertedText);
0057     return item;
0058 }
0059 
0060 void TextSelectionTest::initTestCase()
0061 {
0062     int row = 0;
0063     model.setItem(row++, 0, newItem(QStringLiteral("Line 0")));
0064     model.setItem(row++, 0, newItem(QStringLiteral("Line 1 <b>bold</b>")));
0065     model.setItem(row++, 0, newItem(QStringLiteral("Line 2 <b>bold</b>")));
0066     model.setItem(row++, 0, newItem(QStringLiteral("Line 3")));
0067 }
0068 
0069 void TextSelectionTest::testChangingSelection()
0070 {
0071     // GIVEN
0072 
0073     const QModelIndex index0 = model.index(0, 0);
0074     const QModelIndex index1 = model.index(1, 0);
0075     const QModelIndex index2 = model.index(2, 0);
0076     const QModelIndex index3 = model.index(3, 0);
0077 
0078     TestFactory factory(model.rowCount());
0079     TextSelection selection;
0080     selection.setTextHelperFactory(&factory);
0081     QSignalSpy spy(&selection, &TextSelection::repaintNeeded);
0082 
0083     // WHEN/THEN
0084 
0085     selection.setStart(index1, 3);
0086     QCOMPARE(selection.selectedText(TextSelection::Text), QString());
0087     QVERIFY(!selection.hasSelection());
0088     selection.setEnd(index1, 4);
0089     QCOMPARE(selection.selectedText(TextSelection::Text), QStringLiteral("e"));
0090     QCOMPARE(spy.count(), 0);
0091     QVERIFY(selection.hasSelection());
0092 
0093     selection.setEnd(index1, 9);
0094     QCOMPARE(selection.selectedText(TextSelection::Text), QStringLiteral("e 1 bo"));
0095     QCOMPARE(spy.count(), 0);
0096 
0097     spy.clear();
0098     selection.setEnd(index3, 2);
0099     QCOMPARE(selection.selectedText(TextSelection::Text), QStringLiteral("e 1 bold\nLine 2 bold\nLi"));
0100     QCOMPARE(spy.count(), 2);
0101     QCOMPARE(spy.at(0).at(0).value<QModelIndex>().row(), 1); // line 1 is now fully selected, needs repaint
0102     QCOMPARE(spy.at(1).at(0).value<QModelIndex>().row(), 2); // line 2 was selected too, needs repaint
0103 
0104     spy.clear();
0105     selection.setEnd(index2, 2);
0106     QCOMPARE(selection.selectedText(TextSelection::Text), QStringLiteral("e 1 bold\nLi"));
0107     QCOMPARE(spy.count(), 1);
0108     QCOMPARE(spy.at(0).at(0).value<QModelIndex>().row(), 3); // line 3 is no longer selected
0109 
0110     auto selectionForRow = [&](int row) {
0111         const QModelIndex index = model.index(row, 0);
0112         const QTextCursor cursor = selection.selectionForIndex(index, factory.documentForIndex(index));
0113         return cursor.selection().toPlainText();
0114     };
0115     QCOMPARE(selectionForRow(0), QString());
0116     QCOMPARE(selectionForRow(1), QStringLiteral("e 1 bold"));
0117     QCOMPARE(selectionForRow(2), QStringLiteral("Li"));
0118     QCOMPARE(selectionForRow(3), QString());
0119     QVERIFY(!selection.contains(index0, 0));
0120     QVERIFY(!selection.contains(index1, 2));
0121     QVERIFY(selection.contains(index1, 3));
0122     QVERIFY(selection.contains(index1, 300));
0123     QVERIFY(selection.contains(index2, 1));
0124     QVERIFY(selection.contains(index2, 2)); // (arguable, end of selection)
0125     QVERIFY(!selection.contains(index2, 3));
0126     QVERIFY(!selection.contains(index3, 0));
0127 
0128     // Now move up and reverse selection
0129     spy.clear();
0130     selection.setEnd(index0, 1);
0131     QCOMPARE(selection.selectedText(TextSelection::Text), QStringLiteral("ine 0\nLin"));
0132     QCOMPARE(spy.count(), 2);
0133     QCOMPARE(spy.at(0).at(0).value<QModelIndex>().row(), 1); // line 1's selection is different
0134     QCOMPARE(spy.at(1).at(0).value<QModelIndex>().row(), 2); // line 2 is no longer selected
0135     QCOMPARE(selectionForRow(0), QStringLiteral("ine 0"));
0136     QCOMPARE(selectionForRow(1), QStringLiteral("Lin"));
0137     QCOMPARE(selectionForRow(2), QString());
0138     QCOMPARE(selectionForRow(3), QString());
0139     QVERIFY(!selection.contains(index0, 0));
0140     QVERIFY(selection.contains(index0, 1));
0141     QVERIFY(selection.contains(index0, 100));
0142     QVERIFY(selection.contains(index1, 0));
0143     QVERIFY(selection.contains(index1, 2));
0144     QVERIFY(selection.contains(index1, 3)); // (arguable, end of selection)
0145     QVERIFY(!selection.contains(index2, 0));
0146     QVERIFY(!selection.contains(index3, 0));
0147 }
0148 
0149 void TextSelectionTest::testSingleLineReverseSelection()
0150 {
0151     // GIVEN
0152     const QModelIndex index1 = model.index(1, 0);
0153     TestFactory factory(model.rowCount());
0154     TextSelection selection;
0155     selection.setTextHelperFactory(&factory);
0156 
0157     // WHEN
0158     selection.setStart(index1, 4);
0159     selection.setEnd(index1, 1);
0160 
0161     // THEN
0162     QCOMPARE(selection.selectedText(TextSelection::Text), QStringLiteral("ine"));
0163 }
0164 
0165 void TextSelectionTest::testSelectWordUnderCursor()
0166 {
0167     // GIVEN
0168     const QModelIndex index1 = model.index(1, 0);
0169     const QModelIndex index2 = model.index(2, 0);
0170     TestFactory factory(model.rowCount());
0171     TextSelection selection;
0172     selection.setTextHelperFactory(&factory);
0173 
0174     // WHEN
0175     selection.selectWordUnderCursor(index1, 2, &factory);
0176 
0177     // THEN
0178     QCOMPARE(selection.selectedText(TextSelection::Text), QStringLiteral("Line"));
0179     QVERIFY(selection.contains(index1, 0));
0180     QVERIFY(selection.contains(index1, 2));
0181     QVERIFY(selection.contains(index1, 4));
0182     QVERIFY(!selection.contains(index1, 5));
0183 
0184     // and again on another row
0185 
0186     // WHEN
0187     selection.setStart(index2, 8);
0188     selection.setEnd(index2, 8); // tiny mouse move while double-clicking
0189     QVERIFY(!selection.hasSelection());
0190     selection.selectWordUnderCursor(index2, 8, &factory);
0191 
0192     // THEN
0193     QCOMPARE(selection.selectedText(TextSelection::Text), QStringLiteral("bold"));
0194     QVERIFY(!selection.contains(index2, 0));
0195     QVERIFY(!selection.contains(index2, 6));
0196     QVERIFY(selection.contains(index2, 7));
0197     QVERIFY(selection.contains(index2, 9));
0198 }
0199 
0200 void TextSelectionTest::shouldHaveDefaultValues()
0201 {
0202     TextSelection selection;
0203     QVERIFY(!selection.hasSelection());
0204     QVERIFY(!selection.textHelperFactory());
0205     QVERIFY(selection.attachmentFactories().isEmpty());
0206 }
0207 
0208 void TextSelectionTest::testSelectAll()
0209 {
0210     // GIVEN
0211     const QModelIndex index1 = model.index(1, 0);
0212     TestFactory factory(model.rowCount());
0213     TextSelection selection;
0214     selection.setTextHelperFactory(&factory);
0215 
0216     // WHEN
0217     QVERIFY(!selection.hasSelection());
0218     selection.selectMessage(index1);
0219     QVERIFY(selection.hasSelection());
0220     QCOMPARE(selection.selectedText(TextSelection::Text), QStringLiteral("Line 1 bold"));
0221 }
0222 
0223 void TextSelectionTest::textClear()
0224 {
0225     // GIVEN
0226     const QModelIndex index1 = model.index(1, 0);
0227     TestFactory factory(model.rowCount());
0228     TextSelection selection;
0229     selection.setTextHelperFactory(&factory);
0230 
0231     // WHEN
0232     QVERIFY(!selection.hasSelection());
0233     selection.selectMessage(index1);
0234     QVERIFY(selection.hasSelection());
0235     selection.clear();
0236     QVERIFY(!selection.hasSelection());
0237 }
0238 
0239 #include "moc_textselectiontest.cpp"