File indexing completed on 2024-04-21 15:04:31

0001 /*
0002     This file is part of the KDE libraries
0003     SPDX-FileCopyrightText: 2009 Thomas McGuire <mcguire@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0006 */
0007 
0008 #include "krichtextedittest.h"
0009 
0010 #include <KColorScheme>
0011 #include <krichtextedit.h>
0012 
0013 #include <QFont>
0014 #include <QRegularExpression>
0015 #include <QScrollBar>
0016 #include <QTest>
0017 #include <QTextCursor>
0018 #include <QTextList>
0019 
0020 QTEST_MAIN(KRichTextEditTest)
0021 
0022 void KRichTextEditTest::testLinebreaks()
0023 {
0024     KRichTextEdit edit;
0025     edit.enableRichTextMode();
0026 
0027     // Enter the text with keypresses, for some strange reason a normal setText() or
0028     // setPlainText() call doesn't do the trick
0029     QTest::keyClicks(&edit, QStringLiteral("a\r\r"));
0030     edit.setTextUnderline(true);
0031     QTest::keyClicks(&edit, QStringLiteral("b\r\r\rc"));
0032     QCOMPARE(edit.toPlainText(), QStringLiteral("a\n\nb\n\n\nc"));
0033 
0034     QString html = edit.toCleanHtml();
0035     edit.clear();
0036     edit.setHtml(html);
0037     QCOMPARE(edit.toPlainText(), QStringLiteral("a\n\nb\n\n\nc"));
0038 }
0039 
0040 void KRichTextEditTest::testUpdateLinkAdd()
0041 {
0042     KRichTextEdit edit;
0043     edit.enableRichTextMode();
0044 
0045     // Add text, apply initial formatting, and add a link
0046     QTextCursor cursor = edit.textCursor();
0047     cursor.insertText(QStringLiteral("Test"));
0048     QTextCharFormat charFormat = cursor.charFormat();
0049     // Note that QTextEdit doesn't use the palette. Black is black.
0050     QCOMPARE(charFormat.foreground().color().name(), QColor(Qt::black).name());
0051 
0052     cursor.select(QTextCursor::BlockUnderCursor);
0053     edit.setTextCursor(cursor);
0054     edit.setTextBold(true);
0055     edit.setTextItalic(true);
0056     edit.updateLink(QStringLiteral("http://www.kde.org"), QStringLiteral("KDE"));
0057 
0058     // Validate text and formatting
0059     cursor.movePosition(QTextCursor::Start);
0060     cursor.select(QTextCursor::WordUnderCursor);
0061     edit.setTextCursor(cursor);
0062     QCOMPARE(edit.toPlainText(), QStringLiteral("KDE "));
0063     QCOMPARE(edit.fontItalic(), true);
0064     QCOMPARE(edit.fontWeight(), static_cast<int>(QFont::Bold));
0065     QCOMPARE(edit.fontUnderline(), true);
0066     charFormat = cursor.charFormat();
0067     QCOMPARE(charFormat.foreground(), QBrush(KColorScheme(QPalette::Active, KColorScheme::View).foreground(KColorScheme::LinkText).color()));
0068     QCOMPARE(charFormat.underlineColor(), KColorScheme(QPalette::Active, KColorScheme::View).foreground(KColorScheme::LinkText).color());
0069     QCOMPARE(charFormat.underlineStyle(), QTextCharFormat::SingleUnderline);
0070 }
0071 
0072 void KRichTextEditTest::testUpdateLinkRemove()
0073 {
0074     KRichTextEdit edit;
0075     edit.enableRichTextMode();
0076 
0077     // Add text, apply initial formatting, and add a link
0078     QTextCursor cursor = edit.textCursor();
0079     cursor.insertText(QStringLiteral("Test"));
0080     cursor.select(QTextCursor::BlockUnderCursor);
0081     edit.setTextCursor(cursor);
0082     edit.setTextBold(true);
0083     edit.setTextItalic(true);
0084     edit.updateLink(QStringLiteral("http://www.kde.org"), QStringLiteral("KDE"));
0085 
0086     // Remove link and validate formatting
0087     cursor.movePosition(QTextCursor::Start);
0088     cursor.select(QTextCursor::WordUnderCursor);
0089     edit.setTextCursor(cursor);
0090     edit.updateLink(QString(), QStringLiteral("KDE"));
0091     cursor.movePosition(QTextCursor::Start);
0092     cursor.select(QTextCursor::WordUnderCursor);
0093     edit.setTextCursor(cursor);
0094     QCOMPARE(edit.toPlainText(), QStringLiteral("KDE "));
0095     QCOMPARE(edit.fontItalic(), true);
0096     QCOMPARE(edit.fontWeight(), static_cast<int>(QFont::Bold));
0097     QCOMPARE(edit.fontUnderline(), false);
0098     QTextCharFormat charFormat = cursor.charFormat();
0099     QCOMPARE(charFormat.foreground().color().name(), QColor(Qt::black).name());
0100     QCOMPARE(charFormat.underlineColor().name(), QColor(Qt::black).name());
0101     QCOMPARE(charFormat.underlineStyle(), QTextCharFormat::NoUnderline);
0102 }
0103 
0104 void KRichTextEditTest::testHTMLLineBreaks()
0105 {
0106     KRichTextEdit edit;
0107     edit.enableRichTextMode();
0108 
0109     // Create the following text:
0110     // A
0111     //
0112     // B
0113     QTest::keyClicks(&edit, QStringLiteral("a\r"));
0114 
0115     edit.setTextUnderline(true);
0116 
0117     QTest::keyClicks(&edit, QStringLiteral("\rb"));
0118 
0119     QString html = edit.toCleanHtml();
0120 
0121     // The problem we have is that we need to "fake" being a viewer such
0122     // as Thunderbird or MS-Outlook to unit test our html line breaks.
0123     // For now, we'll parse the 6th line (the empty one) and make sure it has the proper format
0124     // The first four (4) HTML code lines are DOCTYPE through <body> declaration
0125 
0126     const QStringList lines = html.split(QLatin1Char('\n'));
0127 
0128     //  for (int idx=0; idx<lines.size(); idx++) {
0129     //    qDebug() << ( idx + 1 ) << " : " << lines.at( idx );
0130     //  }
0131 
0132 #if QT_VERSION < QT_VERSION_CHECK(6, 3, 0)
0133     QVERIFY2(lines.size() == 7,
0134              "we can't perform this unit test: "
0135              "the html rendering has changed beyond recognition");
0136 #elif QT_VERSION < QT_VERSION_CHECK(6, 4, 0)
0137     QCOMPARE(lines.size(), 8);
0138 #else
0139     QCOMPARE(lines.size(), 10);
0140 #endif
0141 
0142     const QString &line6 = lines.at(lines.size() - 2);
0143 
0144     // make sure that this is an empty <p> line
0145     QVERIFY(line6.startsWith(QStringLiteral("<p style=\"-qt-paragraph-type:empty;")));
0146 
0147     // make sure that empty lines have the &nbsp; inserted
0148     QVERIFY2(line6.endsWith(QStringLiteral(">&nbsp;</p>")),
0149              "Empty lines must have &nbsp; or otherwise 3rd party "
0150              "viewers render those as non-existing lines");
0151 }
0152 
0153 void KRichTextEditTest::testHTMLOrderedLists()
0154 {
0155     // The problem we have is that we need to "fake" being a viewer such
0156     // as Thunderbird or MS-Outlook to unit test our html lists.
0157     // For now, we'll parse the 6th line (the <ol> element) and make sure it has the proper format
0158 
0159     KRichTextEdit edit;
0160     edit.enableRichTextMode();
0161 
0162     edit.setTextUnderline(true);
0163 
0164     // create a numbered (ordered) list
0165     QTextCursor cursor = edit.textCursor();
0166     cursor.insertList(QTextListFormat::ListDecimal);
0167 
0168     QTest::keyClicks(&edit, QStringLiteral("a\rb\rc\r"));
0169 
0170     QString html = edit.toCleanHtml();
0171 
0172     const QStringList lines = html.split(QLatin1Char('\n'));
0173 
0174     //  Uncomment this section in case the first test fails to see if the HTML
0175     //  rendering has actually introduced a bug, or merely a problem with the unit test itself
0176     //
0177     //  for (int idx=0; idx<lines.size(); idx++) {
0178     //    qDebug() << ( idx + 1 ) << " : " << lines.at( idx );
0179     //  }
0180 
0181 #if QT_VERSION < QT_VERSION_CHECK(6, 3, 0)
0182     QVERIFY2(lines.size() == 9,
0183              "we can't perform this unit test: "
0184              "the html rendering has changed beyond recognition");
0185 #elif QT_VERSION < QT_VERSION_CHECK(6, 4, 0)
0186     QCOMPARE(lines.size(), 10);
0187 #else
0188     QCOMPARE(lines.size(), 13);
0189 #endif
0190 
0191     // this is the <ol> declaration line
0192     const QString &line6 = lines.at(lines.size() - 4);
0193 
0194     //  qDebug() << line6;
0195 
0196     const QRegularExpression re(QStringLiteral("<ol.*?margin-left: 0px.*?><li"));
0197     QVERIFY2(!re.match(line6, 0).hasMatch(),
0198              "margin-left: 0px specified for ordered lists "
0199              "removes numbers in 3rd party viewers ");
0200 }
0201 
0202 void KRichTextEditTest::testHTMLUnorderedLists()
0203 {
0204     // The problem we have is that we need to "fake" being a viewer such
0205     // as Thunderbird or MS-Outlook to unit test our html lists.
0206     // For now, we'll parse the 6th line (the <ul> element) and make sure it has the proper format
0207     // The first four (4) HTML code lines are DOCTYPE through <body> declaration
0208 
0209     KRichTextEdit edit;
0210     edit.enableRichTextMode();
0211 
0212     edit.setTextUnderline(true);
0213 
0214     // create a numbered (ordered) list
0215     QTextCursor cursor = edit.textCursor();
0216     cursor.insertList(QTextListFormat::ListDisc);
0217 
0218     QTest::keyClicks(&edit, QStringLiteral("a\rb\rc\r"));
0219 
0220     QString html = edit.toCleanHtml();
0221 
0222     const QStringList lines = html.split(QLatin1Char('\n'));
0223 
0224     //  Uncomment this section in case the first test fails to see if the HTML
0225     //  rendering has actually introduced a bug, or merely a problem with the unit test itself
0226     //
0227     //  for (int idx=0; idx<lines.size(); idx++) {
0228     //    qDebug() << ( idx + 1 ) << " : " << lines.at( idx );
0229     //  }
0230 
0231 #if QT_VERSION < QT_VERSION_CHECK(6, 3, 0)
0232     QVERIFY2(lines.size() == 9,
0233              "we can't perform this unit test: "
0234              "the html rendering has changed beyond recognition");
0235 #elif QT_VERSION < QT_VERSION_CHECK(6, 4, 0)
0236     QCOMPARE(lines.size(), 10);
0237 #else
0238     QCOMPARE(lines.size(), 13);
0239 #endif
0240 
0241     // this is the <ol> declaration line
0242     const QString &line6 = lines.at(lines.size() - 4);
0243 
0244     //  qDebug() << line6;
0245 
0246 #if KTEXTWIDGETS_BUILD_DEPRECATED_SINCE(5, 70)
0247     // there should not be a margin-left: 0 defined for the <ol> element
0248     QRegExp regex(QStringLiteral("<ul.*margin-left: 0px.*><li"));
0249     regex.setMinimal(true);
0250     QVERIFY2(regex.indexIn(line6, 0) == -1,
0251              "margin-left: 0px specified for unordered lists "
0252              "removes numbers in 3rd party viewers ");
0253 #endif
0254 
0255     const QRegularExpression re(QStringLiteral("<ul.*?margin-left: 0px.*?><li"));
0256     QVERIFY2(!re.match(line6, 0).hasMatch(),
0257              "margin-left: 0px specified for unordered lists "
0258              "removes numbers in 3rd party viewers ");
0259 }
0260 
0261 void KRichTextEditTest::testHeading()
0262 {
0263     KRichTextEdit edit;
0264     // Create two lines, make second one a heading
0265     QTest::keyClicks(&edit, QStringLiteral("a\rb"));
0266     // Make sure heading actually changes
0267     edit.setHeadingLevel(1);
0268     QCOMPARE(edit.textCursor().blockFormat().headingLevel(), 1);
0269     QCOMPARE(edit.fontWeight(), static_cast<int>(QFont::Bold));
0270     // Make sure it doesn't clutter undo stack (a single undo is sufficient)
0271     edit.undo();
0272     QCOMPARE(edit.textCursor().blockFormat().headingLevel(), 0);
0273     QCOMPARE(edit.fontWeight(), static_cast<int>(QFont::Normal));
0274 
0275     // Set heading & keep writing, the text remains a heading
0276     edit.setHeadingLevel(2);
0277     QTest::keyClicks(&edit, QStringLiteral("cd"));
0278     QCOMPARE(edit.textCursor().blockFormat().headingLevel(), 2);
0279     QCOMPARE(edit.fontWeight(), static_cast<int>(QFont::Bold));
0280 
0281     // Now add a new line, make sure it's no longer a heading
0282     QTest::keyClick(&edit, '\r');
0283     QCOMPARE(edit.textCursor().blockFormat().headingLevel(), 0);
0284     QCOMPARE(edit.fontWeight(), static_cast<int>(QFont::Normal));
0285 
0286     // Make sure creating new line is also undoable
0287     edit.undo();
0288     QCOMPARE(edit.textCursor().position(), 5);
0289     QCOMPARE(edit.textCursor().blockFormat().headingLevel(), 2);
0290     QCOMPARE(edit.fontWeight(), static_cast<int>(QFont::Bold));
0291 
0292     // Add a new line and some more text, make sure it's still normal
0293     QTest::keyClicks(&edit, QStringLiteral("\ref"));
0294     QCOMPARE(edit.textCursor().blockFormat().headingLevel(), 0);
0295     QCOMPARE(edit.fontWeight(), static_cast<int>(QFont::Normal));
0296 
0297     // Go to beginning of this line, press Backspace -> lines should be merged,
0298     // current line should become a heading
0299     QTest::keyClick(&edit, Qt::Key_Home);
0300     QTest::keyClick(&edit, Qt::Key_Backspace);
0301     QCOMPARE(edit.textCursor().blockFormat().headingLevel(), 2);
0302     QCOMPARE(edit.fontWeight(), static_cast<int>(QFont::Bold));
0303     // Make sure this is also undoable
0304     edit.undo();
0305     QCOMPARE(edit.textCursor().blockFormat().headingLevel(), 0);
0306     QCOMPARE(edit.fontWeight(), static_cast<int>(QFont::Normal));
0307     // Return it back
0308     QTest::keyClick(&edit, Qt::Key_Backspace);
0309     // The line is now "bcd|ef", "|" is cursor. Press Enter, the second line should remain a heading
0310     QTest::keyClick(&edit, Qt::Key_Return);
0311     QCOMPARE(edit.textCursor().blockFormat().headingLevel(), 2);
0312     QCOMPARE(edit.fontWeight(), static_cast<int>(QFont::Bold));
0313     // Change the heading level back to normal
0314     edit.setHeadingLevel(0);
0315     QCOMPARE(edit.textCursor().blockFormat().headingLevel(), 0);
0316     QCOMPARE(edit.fontWeight(), static_cast<int>(QFont::Normal));
0317     // Go to end of previous line, press Delete -> lines should be merged again
0318     QTextCursor cursor = edit.textCursor();
0319     cursor.movePosition(QTextCursor::PreviousBlock);
0320     cursor.movePosition(QTextCursor::EndOfBlock);
0321     edit.setTextCursor(cursor);
0322     QTest::keyClick(&edit, Qt::Key_Delete);
0323     QCOMPARE(edit.textCursor().blockFormat().headingLevel(), 2);
0324     QCOMPARE(edit.fontWeight(), static_cast<int>(QFont::Bold));
0325     // Make sure this is also undoable
0326     edit.undo();
0327     QCOMPARE(edit.textCursor().blockFormat().headingLevel(), 0);
0328     QCOMPARE(edit.fontWeight(), static_cast<int>(QFont::Normal));
0329 
0330     // Now playing with selection. The contents are currently:
0331     // ---
0332     // a
0333     // bcd
0334     // ef
0335     // gh
0336     // ---
0337     // Let's add a new line 'gh', select everything between "c" and "g"
0338     // and change heading. It should apply to both lines
0339     QTest::keyClicks(&edit, QStringLiteral("\rgh"));
0340     cursor.setPosition(4, QTextCursor::MoveAnchor);
0341     cursor.setPosition(10, QTextCursor::KeepAnchor);
0342     edit.setTextCursor(cursor);
0343     edit.setHeadingLevel(5);
0344     // In the end, both lines should change the heading (even before the selection, i.e. 'b'!)
0345     cursor.setPosition(3);
0346     edit.setTextCursor(cursor);
0347     QCOMPARE(edit.textCursor().blockFormat().headingLevel(), 5);
0348     QCOMPARE(edit.fontWeight(), static_cast<int>(QFont::Bold));
0349     // (and after the selection, i.e. 'f'!)
0350     cursor.setPosition(11);
0351     edit.setTextCursor(cursor);
0352     QCOMPARE(edit.textCursor().blockFormat().headingLevel(), 5);
0353     QCOMPARE(edit.fontWeight(), static_cast<int>(QFont::Bold));
0354 }
0355 
0356 void KRichTextEditTest::testRulerScroll()
0357 {
0358     // This is a test for bug 195828
0359     KRichTextEdit edit;
0360     // Add some lines, so that scroll definitely appears
0361     for (int i = 0; i < 100; i++) {
0362         QTest::keyClicks(&edit, QStringLiteral("New line\r"));
0363     }
0364     // Widget has to be shown for the scrollbar to be adjusted
0365     edit.show();
0366     // Ensure the scrollbar actually appears
0367     QVERIFY(edit.verticalScrollBar()->value() > 0);
0368 
0369     edit.insertHorizontalRule();
0370     // Make sure scrollbar didn't jump to the top
0371     QVERIFY(edit.verticalScrollBar()->value() > 0);
0372 }
0373 
0374 void KRichTextEditTest::testNestedLists()
0375 {
0376     KRichTextEdit edit;
0377     // Simplest test: create a list with a single element
0378     QTest::keyClicks(&edit, QStringLiteral("el1"));
0379     edit.setListStyle(-static_cast<int>(QTextListFormat::ListSquare));
0380     QVERIFY(edit.textCursor().currentList());
0381     QCOMPARE(edit.textCursor().currentList()->format().style(), QTextListFormat::ListSquare);
0382     // It should not be indentable, as there is nothing above
0383     QVERIFY(!edit.canIndentList());
0384     // But it should be dedentable
0385     QVERIFY(edit.canDedentList());
0386     // Press enter, a new element should be added
0387     QTest::keyClicks(&edit, QStringLiteral("\rel2"));
0388     QVERIFY(edit.textCursor().currentList());
0389     QCOMPARE(edit.textCursor().currentList()->format().style(), QTextListFormat::ListSquare);
0390     // Change indentation
0391     edit.indentListMore();
0392     edit.setListStyle(-static_cast<int>(QTextListFormat::ListCircle));
0393     QCOMPARE(edit.textCursor().currentList()->format().indent(), 2);
0394     QCOMPARE(edit.textCursor().currentList()->format().style(), QTextListFormat::ListCircle);
0395     // And another one; let's then change the style of "3" and see if "2" have also changed style
0396     QTest::keyClicks(&edit, QStringLiteral("\rel3"));
0397     edit.setListStyle(-static_cast<int>(QTextListFormat::ListDecimal));
0398     edit.moveCursor(QTextCursor::PreviousBlock);
0399     QCOMPARE(edit.textCursor().currentList()->format().style(), QTextListFormat::ListDecimal);
0400     // Now add another element, and dedent it, so the list should look like following:
0401     // [] el1
0402     //    1. el2
0403     //    2. el3
0404     // [] el4
0405     edit.moveCursor(QTextCursor::End);
0406     QTest::keyClicks(&edit, QStringLiteral("\rel4"));
0407     edit.indentListLess();
0408     QCOMPARE(edit.textCursor().currentList()->format().style(), QTextListFormat::ListSquare);
0409     // Let's change the style to disc and see if first element have also changed the style
0410     edit.setListStyle(-static_cast<int>(QTextListFormat::ListDisc));
0411     edit.moveCursor(QTextCursor::Start);
0412     QCOMPARE(edit.textCursor().currentList()->format().style(), QTextListFormat::ListDisc);
0413     // Now let's play with selection. First we add couple subelements below, so the list is:
0414     // *  el1
0415     //    1. el2
0416     //    2. el3
0417     // *  el4
0418     //    o  el5
0419     //    o  el6
0420     edit.moveCursor(QTextCursor::End);
0421     QTest::keyClicks(&edit, QStringLiteral("\rel5"));
0422     edit.indentListMore();
0423     edit.setListStyle(-static_cast<int>(QTextListFormat::ListCircle));
0424     QTest::keyClicks(&edit, QStringLiteral("\rel6"));
0425 
0426     // Let's select (el3-el5) and indent them. It should become:
0427     // * el1
0428     //   1. el2
0429     //      1. el3
0430     //   2. el4
0431     //      o  el5
0432     //   3. el6
0433     QTextCursor cursor(edit.document());
0434     cursor.setPosition(9);
0435     cursor.setPosition(17, QTextCursor::KeepAnchor);
0436     edit.setTextCursor(cursor);
0437     edit.indentListMore();
0438     edit.moveCursor(QTextCursor::End);
0439     QCOMPARE(edit.textCursor().currentList()->count(), 3);
0440     QCOMPARE(edit.textCursor().currentList()->format().style(), QTextListFormat::ListDecimal);
0441     // Now select el2-el5 and dedent them. It should become:
0442     // *  el1
0443     // *  el2
0444     //    1. el3
0445     // *  el4
0446     //    o  el5
0447     //    o  el6
0448     cursor.setPosition(6);
0449     cursor.setPosition(18, QTextCursor::KeepAnchor);
0450     edit.setTextCursor(cursor);
0451     edit.indentListLess();
0452     edit.moveCursor(QTextCursor::End);
0453     QCOMPARE(edit.textCursor().currentList()->count(), 2);
0454     QCOMPARE(edit.textCursor().currentList()->format().style(), QTextListFormat::ListCircle);
0455     // point at "el4"
0456     cursor.setPosition(13);
0457     QCOMPARE(cursor.currentList()->count(), 3);
0458     QCOMPARE(cursor.currentList()->format().style(), QTextListFormat::ListDisc);
0459     // Select el4 && el5, dedent it, so el4 becomes a simple text:
0460     // *  el1
0461     // *  el2
0462     //    1. el3
0463     // el4
0464     // o  el5
0465     //    o  el6
0466     cursor.setPosition(17, QTextCursor::KeepAnchor);
0467     edit.setTextCursor(cursor);
0468     edit.indentListLess();
0469     // point cursor at "el4"
0470     cursor.setPosition(13);
0471     QVERIFY(!cursor.currentList());
0472     // point at "el5", make sure it's a separate list now
0473     cursor.setPosition(16);
0474     QCOMPARE(cursor.currentList()->count(), 1);
0475     QCOMPARE(cursor.currentList()->format().style(), QTextListFormat::ListCircle);
0476     // Make sure the selection is not dedentable anymore
0477     QVERIFY(!edit.canDedentList());
0478 }
0479 
0480 #include "moc_krichtextedittest.cpp"