| 1 | /**************************************************************************** |
| 2 | ** |
| 3 | ** Copyright (C) 2016 The Qt Company Ltd. |
| 4 | ** Contact: https://www.qt.io/licensing/ |
| 5 | ** |
| 6 | ** This file is part of the test suite of the Qt Toolkit. |
| 7 | ** |
| 8 | ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ |
| 9 | ** Commercial License Usage |
| 10 | ** Licensees holding valid commercial Qt licenses may use this file in |
| 11 | ** accordance with the commercial license agreement provided with the |
| 12 | ** Software or, alternatively, in accordance with the terms contained in |
| 13 | ** a written agreement between you and The Qt Company. For licensing terms |
| 14 | ** and conditions see https://www.qt.io/terms-conditions. For further |
| 15 | ** information use the contact form at https://www.qt.io/contact-us. |
| 16 | ** |
| 17 | ** GNU General Public License Usage |
| 18 | ** Alternatively, this file may be used under the terms of the GNU |
| 19 | ** General Public License version 3 as published by the Free Software |
| 20 | ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT |
| 21 | ** included in the packaging of this file. Please review the following |
| 22 | ** information to ensure the GNU General Public License requirements will |
| 23 | ** be met: https://www.gnu.org/licenses/gpl-3.0.html. |
| 24 | ** |
| 25 | ** $QT_END_LICENSE$ |
| 26 | ** |
| 27 | ****************************************************************************/ |
| 28 | |
| 29 | |
| 30 | #include <QtTest/QtTest> |
| 31 | |
| 32 | #include <qtextdocument.h> |
| 33 | #include <qtextdocumentfragment.h> |
| 34 | #include <qtextlist.h> |
| 35 | #include <qabstracttextdocumentlayout.h> |
| 36 | #include <qtextcursor.h> |
| 37 | #include "../qtextdocument/common.h" |
| 38 | |
| 39 | class tst_QTextList : public QObject |
| 40 | { |
| 41 | Q_OBJECT |
| 42 | |
| 43 | private slots: |
| 44 | void init(); |
| 45 | void cleanup(); |
| 46 | void item(); |
| 47 | void autoNumbering(); |
| 48 | void autoNumberingRTL(); |
| 49 | void autoNumberingPrefixAndSuffix(); |
| 50 | void autoNumberingPrefixAndSuffixRTL(); |
| 51 | void autoNumberingPrefixAndSuffixHtmlExportImport(); |
| 52 | void romanNumbering(); |
| 53 | void romanNumberingLimit(); |
| 54 | void formatChange(); |
| 55 | void cursorNavigation(); |
| 56 | void partialRemoval(); |
| 57 | void formatReferenceChange(); |
| 58 | void ensureItemOrder(); |
| 59 | void add(); |
| 60 | void defaultIndent(); |
| 61 | void blockUpdate(); |
| 62 | void numbering_data(); |
| 63 | void numbering(); |
| 64 | |
| 65 | private: |
| 66 | QTextDocument *doc; |
| 67 | QTextCursor cursor; |
| 68 | QTestDocumentLayout *layout; |
| 69 | }; |
| 70 | |
| 71 | void tst_QTextList::init() |
| 72 | { |
| 73 | doc = new QTextDocument(); |
| 74 | layout = new QTestDocumentLayout(doc); |
| 75 | doc->setDocumentLayout(layout); |
| 76 | cursor = QTextCursor(doc); |
| 77 | } |
| 78 | |
| 79 | void tst_QTextList::cleanup() |
| 80 | { |
| 81 | cursor = QTextCursor(); |
| 82 | delete doc; |
| 83 | doc = 0; |
| 84 | } |
| 85 | |
| 86 | void tst_QTextList::item() |
| 87 | { |
| 88 | // this is basically a test for the key() + 1 in QTextList::item. |
| 89 | QTextList *list = cursor.createList(format: QTextListFormat()); |
| 90 | QVERIFY(list->item(0).blockFormat().objectIndex() != -1); |
| 91 | } |
| 92 | |
| 93 | void tst_QTextList::autoNumbering() |
| 94 | { |
| 95 | QTextListFormat fmt; |
| 96 | fmt.setStyle(QTextListFormat::ListLowerAlpha); |
| 97 | QTextList *list = cursor.createList(format: fmt); |
| 98 | QVERIFY(list); |
| 99 | |
| 100 | for (int i = 0; i < 27; ++i) |
| 101 | cursor.insertBlock(); |
| 102 | |
| 103 | QCOMPARE(list->count(), 28); |
| 104 | |
| 105 | QVERIFY(cursor.currentList()); |
| 106 | QCOMPARE(cursor.currentList()->itemNumber(cursor.block()), 27); |
| 107 | QCOMPARE(cursor.currentList()->itemText(cursor.block()), QLatin1String("ab." )); |
| 108 | } |
| 109 | |
| 110 | void tst_QTextList::autoNumberingPrefixAndSuffix() |
| 111 | { |
| 112 | QTextListFormat fmt; |
| 113 | fmt.setStyle(QTextListFormat::ListLowerAlpha); |
| 114 | fmt.setNumberPrefix("-" ); |
| 115 | fmt.setNumberSuffix(")" ); |
| 116 | QTextList *list = cursor.createList(format: fmt); |
| 117 | QVERIFY(list); |
| 118 | |
| 119 | for (int i = 0; i < 27; ++i) |
| 120 | cursor.insertBlock(); |
| 121 | |
| 122 | QCOMPARE(list->count(), 28); |
| 123 | |
| 124 | QVERIFY(cursor.currentList()); |
| 125 | QCOMPARE(cursor.currentList()->itemNumber(cursor.block()), 27); |
| 126 | QCOMPARE(cursor.currentList()->itemText(cursor.block()), QLatin1String("-ab)" )); |
| 127 | } |
| 128 | |
| 129 | void tst_QTextList::autoNumberingPrefixAndSuffixRTL() |
| 130 | { |
| 131 | QTextBlockFormat bfmt; |
| 132 | bfmt.setLayoutDirection(Qt::RightToLeft); |
| 133 | cursor.setBlockFormat(bfmt); |
| 134 | |
| 135 | QTextListFormat fmt; |
| 136 | fmt.setStyle(QTextListFormat::ListUpperAlpha); |
| 137 | fmt.setNumberPrefix("-" ); |
| 138 | fmt.setNumberSuffix("*" ); |
| 139 | QTextList *list = cursor.createList(format: fmt); |
| 140 | QVERIFY(list); |
| 141 | |
| 142 | cursor.insertBlock(); |
| 143 | |
| 144 | QCOMPARE(list->count(), 2); |
| 145 | |
| 146 | QCOMPARE(cursor.currentList()->itemText(cursor.block()), QLatin1String("*B-" )); |
| 147 | } |
| 148 | |
| 149 | void tst_QTextList::autoNumberingPrefixAndSuffixHtmlExportImport() |
| 150 | { |
| 151 | QTextListFormat fmt; |
| 152 | fmt.setStyle(QTextListFormat::ListLowerAlpha); |
| 153 | fmt.setNumberPrefix("\"" ); |
| 154 | fmt.setNumberSuffix("#" ); |
| 155 | fmt.setIndent(10); |
| 156 | // FIXME: Would like to test "'" but there's a problem in the css parser (Scanner::preprocess |
| 157 | // is called before the values are being parsed), so the quoting does not work. |
| 158 | QTextList *list = cursor.createList(format: fmt); |
| 159 | QVERIFY(list); |
| 160 | |
| 161 | for (int i = 0; i < 27; ++i) |
| 162 | cursor.insertBlock(); |
| 163 | |
| 164 | QCOMPARE(list->count(), 28); |
| 165 | |
| 166 | QString htmlExport = doc->toHtml(); |
| 167 | QTextDocument importDoc; |
| 168 | importDoc.setHtml(htmlExport); |
| 169 | |
| 170 | QTextCursor importCursor(&importDoc); |
| 171 | for (int i = 0; i < 27; ++i) |
| 172 | importCursor.movePosition(op: QTextCursor::NextBlock); |
| 173 | |
| 174 | QVERIFY(importCursor.currentList()); |
| 175 | QCOMPARE(importCursor.currentList()->itemNumber(importCursor.block()), 27); |
| 176 | QCOMPARE(importCursor.currentList()->itemText(importCursor.block()), QLatin1String("\"ab#" )); |
| 177 | QCOMPARE(importCursor.currentList()->format().indent(), 10); |
| 178 | } |
| 179 | |
| 180 | void tst_QTextList::autoNumberingRTL() |
| 181 | { |
| 182 | QTextBlockFormat bfmt; |
| 183 | bfmt.setLayoutDirection(Qt::RightToLeft); |
| 184 | cursor.setBlockFormat(bfmt); |
| 185 | |
| 186 | QTextListFormat fmt; |
| 187 | fmt.setStyle(QTextListFormat::ListUpperAlpha); |
| 188 | QTextList *list = cursor.createList(format: fmt); |
| 189 | QVERIFY(list); |
| 190 | |
| 191 | cursor.insertBlock(); |
| 192 | |
| 193 | QCOMPARE(list->count(), 2); |
| 194 | |
| 195 | QCOMPARE(cursor.currentList()->itemText(cursor.block()), QLatin1String(".B" )); |
| 196 | } |
| 197 | |
| 198 | void tst_QTextList::romanNumbering() |
| 199 | { |
| 200 | QTextListFormat fmt; |
| 201 | fmt.setStyle(QTextListFormat::ListUpperRoman); |
| 202 | QTextList *list = cursor.createList(format: fmt); |
| 203 | QVERIFY(list); |
| 204 | |
| 205 | for (int i = 0; i < 4998; ++i) |
| 206 | cursor.insertBlock(); |
| 207 | |
| 208 | QCOMPARE(list->count(), 4999); |
| 209 | |
| 210 | QVERIFY(cursor.currentList()); |
| 211 | QCOMPARE(cursor.currentList()->itemNumber(cursor.block()), 4998); |
| 212 | QCOMPARE(cursor.currentList()->itemText(cursor.block()), QLatin1String("MMMMCMXCIX." )); |
| 213 | } |
| 214 | |
| 215 | void tst_QTextList::romanNumberingLimit() |
| 216 | { |
| 217 | QTextListFormat fmt; |
| 218 | fmt.setStyle(QTextListFormat::ListLowerRoman); |
| 219 | QTextList *list = cursor.createList(format: fmt); |
| 220 | QVERIFY(list); |
| 221 | |
| 222 | for (int i = 0; i < 4999; ++i) |
| 223 | cursor.insertBlock(); |
| 224 | |
| 225 | QCOMPARE(list->count(), 5000); |
| 226 | |
| 227 | QVERIFY(cursor.currentList()); |
| 228 | QCOMPARE(cursor.currentList()->itemNumber(cursor.block()), 4999); |
| 229 | QCOMPARE(cursor.currentList()->itemText(cursor.block()), QLatin1String("?." )); |
| 230 | } |
| 231 | |
| 232 | void tst_QTextList::formatChange() |
| 233 | { |
| 234 | // testing the formatChanged slot in QTextListManager |
| 235 | |
| 236 | /* <initial block> |
| 237 | * 1. |
| 238 | * 2. |
| 239 | */ |
| 240 | QTextList *list = cursor.insertList(style: QTextListFormat::ListDecimal); |
| 241 | QTextList *firstList = list; |
| 242 | cursor.insertBlock(); |
| 243 | |
| 244 | QVERIFY(list && list->count() == 2); |
| 245 | |
| 246 | QTextBlockFormat bfmt = cursor.blockFormat(); |
| 247 | // QCOMPARE(bfmt.object(), list); |
| 248 | |
| 249 | bfmt.setObjectIndex(-1); |
| 250 | cursor.setBlockFormat(bfmt); |
| 251 | |
| 252 | QCOMPARE(firstList->count(), 1); |
| 253 | } |
| 254 | |
| 255 | void tst_QTextList::cursorNavigation() |
| 256 | { |
| 257 | // testing some cursor list methods |
| 258 | |
| 259 | /* <initial block> |
| 260 | * 1. |
| 261 | * 2. |
| 262 | */ |
| 263 | cursor.insertList(style: QTextListFormat::ListDecimal); |
| 264 | cursor.insertBlock(); |
| 265 | |
| 266 | cursor.movePosition(op: QTextCursor::Start); |
| 267 | cursor.movePosition(op: QTextCursor::NextBlock); |
| 268 | cursor.movePosition(op: QTextCursor::NextBlock); |
| 269 | QVERIFY(cursor.currentList()); |
| 270 | cursor.movePosition(op: QTextCursor::PreviousBlock); |
| 271 | QVERIFY(cursor.currentList()); |
| 272 | QCOMPARE(cursor.currentList()->itemNumber(cursor.block()), 0); |
| 273 | } |
| 274 | |
| 275 | void tst_QTextList::partialRemoval() |
| 276 | { |
| 277 | /* this is essentially a test for PieceTable::removeBlock to not miss any |
| 278 | blocks with the blockChanged signal emission that actually get removed. |
| 279 | |
| 280 | It creates two lists, like this: |
| 281 | |
| 282 | 1. Hello World |
| 283 | a. Foobar |
| 284 | |
| 285 | and then removes from within the 'Hello World' into the 'Foobar' . |
| 286 | There used to be no emission for the removal of the second (a.) block, |
| 287 | causing list inconsistencies. |
| 288 | |
| 289 | */ |
| 290 | |
| 291 | QTextList *firstList = cursor.insertList(style: QTextListFormat::ListDecimal); |
| 292 | |
| 293 | QTextCursor selStart = cursor; |
| 294 | selStart.movePosition(op: QTextCursor::PreviousCharacter); |
| 295 | |
| 296 | cursor.insertText(text: "Hello World" ); |
| 297 | |
| 298 | // position it well into the 'hello world' text. |
| 299 | selStart.movePosition(op: QTextCursor::NextCharacter); |
| 300 | selStart.movePosition(op: QTextCursor::NextCharacter); |
| 301 | selStart.clearSelection(); |
| 302 | |
| 303 | QPointer<QTextList> secondList = cursor.insertList(style: QTextListFormat::ListCircle); |
| 304 | cursor.insertText(text: "Foobar" ); |
| 305 | |
| 306 | // position it into the 'foo bar' text. |
| 307 | cursor.movePosition(op: QTextCursor::PreviousCharacter); |
| 308 | QTextCursor selEnd = cursor; |
| 309 | |
| 310 | // this creates a selection that includes parts of both text-fragments and also the list item of the second list. |
| 311 | QTextCursor selection = selStart; |
| 312 | selection.setPosition(pos: selEnd.position(), mode: QTextCursor::KeepAnchor); |
| 313 | |
| 314 | selection.deleteChar(); // deletes the second list |
| 315 | |
| 316 | QVERIFY(!secondList); |
| 317 | QVERIFY(firstList->count() > 0); |
| 318 | |
| 319 | doc->undo(); |
| 320 | } |
| 321 | |
| 322 | void tst_QTextList::formatReferenceChange() |
| 323 | { |
| 324 | QTextList *list = cursor.insertList(style: QTextListFormat::ListDecimal); |
| 325 | cursor.insertText(text: "Some Content..." ); |
| 326 | cursor.insertBlock(format: QTextBlockFormat()); |
| 327 | |
| 328 | cursor.setPosition(pos: list->item(i: 0).position()); |
| 329 | int listItemStartPos = cursor.position(); |
| 330 | cursor.movePosition(op: QTextCursor::NextBlock); |
| 331 | int listItemLen = cursor.position() - listItemStartPos; |
| 332 | layout->expect(from: listItemStartPos, oldLength: listItemLen, length: listItemLen); |
| 333 | |
| 334 | QTextListFormat fmt = list->format(); |
| 335 | fmt.setStyle(QTextListFormat::ListCircle); |
| 336 | list->setFormat(fmt); |
| 337 | |
| 338 | QVERIFY(layout->called); |
| 339 | QVERIFY(!layout->error); |
| 340 | } |
| 341 | |
| 342 | void tst_QTextList::ensureItemOrder() |
| 343 | { |
| 344 | /* |
| 345 | * Insert a new list item before the first one and verify the blocks |
| 346 | * are sorted after that. |
| 347 | */ |
| 348 | QTextList *list = cursor.insertList(style: QTextListFormat::ListDecimal); |
| 349 | |
| 350 | QTextBlockFormat fmt = cursor.blockFormat(); |
| 351 | cursor.movePosition(op: QTextCursor::Start); |
| 352 | cursor.insertBlock(format: fmt); |
| 353 | |
| 354 | QCOMPARE(list->item(0).position(), 1); |
| 355 | QCOMPARE(list->item(1).position(), 2); |
| 356 | } |
| 357 | |
| 358 | void tst_QTextList::add() |
| 359 | { |
| 360 | QTextList *list = cursor.insertList(style: QTextListFormat::ListDecimal); |
| 361 | cursor.insertBlock(format: QTextBlockFormat()); |
| 362 | QCOMPARE(list->count(), 1); |
| 363 | cursor.insertBlock(format: QTextBlockFormat()); |
| 364 | list->add(block: cursor.block()); |
| 365 | QCOMPARE(list->count(), 2); |
| 366 | } |
| 367 | |
| 368 | // Task #72036 |
| 369 | void tst_QTextList::defaultIndent() |
| 370 | { |
| 371 | QTextListFormat fmt; |
| 372 | QCOMPARE(fmt.indent(), 1); |
| 373 | } |
| 374 | |
| 375 | void tst_QTextList::blockUpdate() |
| 376 | { |
| 377 | // three items |
| 378 | QTextList *list = cursor.insertList(style: QTextListFormat::ListDecimal); |
| 379 | cursor.insertBlock(); |
| 380 | cursor.insertBlock(); |
| 381 | |
| 382 | // remove second, needs also update on the third |
| 383 | // since the numbering might have changed |
| 384 | const int len = cursor.position() + cursor.block().length() - 1; |
| 385 | layout->expect(from: 1, oldLength: len, length: len); |
| 386 | list->remove(list->item(i: 1)); |
| 387 | QVERIFY(!layout->error); |
| 388 | } |
| 389 | |
| 390 | void tst_QTextList::numbering_data() |
| 391 | { |
| 392 | QTest::addColumn<int>(name: "format" ); |
| 393 | QTest::addColumn<int>(name: "number" ); |
| 394 | QTest::addColumn<QString>(name: "result" ); |
| 395 | |
| 396 | QTest::newRow(dataTag: "E." ) << int(QTextListFormat::ListUpperAlpha) << 5 << "E." ; |
| 397 | QTest::newRow(dataTag: "abc." ) << int(QTextListFormat::ListLowerAlpha) << (26 + 2) * 26 + 3 << "abc." ; |
| 398 | QTest::newRow(dataTag: "12." ) << int(QTextListFormat::ListDecimal) << 12 << "12." ; |
| 399 | QTest::newRow(dataTag: "XXIV." ) << int(QTextListFormat::ListUpperRoman) << 24 << "XXIV." ; |
| 400 | QTest::newRow(dataTag: "VIII." ) << int(QTextListFormat::ListUpperRoman) << 8 << "VIII." ; |
| 401 | QTest::newRow(dataTag: "xxx." ) << int(QTextListFormat::ListLowerRoman) << 30 << "xxx." ; |
| 402 | QTest::newRow(dataTag: "xxix." ) << int(QTextListFormat::ListLowerRoman) << 29 << "xxix." ; |
| 403 | // QTest::newRow("xxx. alpha") << int(QTextListFormat::ListLowerAlpha) << (24 * 26 + 24) * 26 + 24 << "xxx."; //Too slow |
| 404 | } |
| 405 | |
| 406 | void tst_QTextList::numbering() |
| 407 | { |
| 408 | QFETCH(int, format); |
| 409 | QFETCH(int, number); |
| 410 | QFETCH(QString, result); |
| 411 | |
| 412 | |
| 413 | QTextListFormat fmt; |
| 414 | fmt.setStyle(QTextListFormat::Style(format)); |
| 415 | QTextList *list = cursor.createList(format: fmt); |
| 416 | QVERIFY(list); |
| 417 | |
| 418 | for (int i = 1; i < number; ++i) |
| 419 | cursor.insertBlock(); |
| 420 | |
| 421 | QCOMPARE(list->count(), number); |
| 422 | |
| 423 | QVERIFY(cursor.currentList()); |
| 424 | QCOMPARE(cursor.currentList()->itemNumber(cursor.block()), number - 1); |
| 425 | QCOMPARE(cursor.currentList()->itemText(cursor.block()), result); |
| 426 | } |
| 427 | |
| 428 | QTEST_MAIN(tst_QTextList) |
| 429 | #include "tst_qtextlist.moc" |
| 430 | |