parttest.cpp 83.7 KB
Newer Older
1 2
/***************************************************************************
 *   Copyright (C) 2013 by Albert Astals Cid <aacid@kde.org>               *
3 4 5
 *   Copyright (C) 2017    Klarälvdalens Datakonsult AB, a KDAB Group      *
 *                         company, info@kdab.com. Work sponsored by the   *
 *                         LiMux project of the city of Munich             *
6 7 8 9 10 11 12
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 ***************************************************************************/

13 14
// clazy:excludeall=qstring-allocations

15
#include <QtTest>
16

17
#include "../core/annotations.h"
18
#include "../core/form.h"
19
#include "../core/page.h"
20 21
#include "../part.h"
#include "../ui/toc.h"
22
#include "../ui/sidebar.h"
23
#include "../ui/pageview.h"
24
#include "../ui/presentationwidget.h"
David Hurka's avatar
David Hurka committed
25
#include "../ui/toggleactionmenu.h"
26
#include "../settings.h"
27
#include "closedialoghelper.h"
28

29 30
#include "../generators/poppler/config-okular-poppler.h"

31
#include <KActionCollection>
32
#include <KConfigDialog>
33
#include <KParts/OpenUrlArguments>
34

35
#include <QApplication>
36
#include <QClipboard>
37 38
#include <QMessageBox>
#include <QPushButton>
Albert Astals Cid's avatar
Albert Astals Cid committed
39
#include <QScrollBar>
40
#include <QTabletEvent>
41
#include <QTemporaryDir>
42
#include <QTextEdit>
43
#include <QTreeView>
44 45 46
#include <QUrl>
#include <QDesktopServices>
#include <QMenu>
David Hurka's avatar
David Hurka committed
47
#include <QToolBar>
48 49 50 51 52 53 54 55

namespace Okular
{
class PartTest
    : public QObject
{
    Q_OBJECT

56 57
        static bool openDocument(Okular::Part *part, const QString &filePath);

58
    signals:
59
        void urlHandler(const QUrl &url); // NOLINT(readability-inconsistent-declaration-parameter-name)
60

61
    private slots:
62
        void testReload();
63
        void testCanceledReload();
64
        void testTOCReload();
rkflx's avatar
rkflx committed
65 66
        void testForwardPDF();
        void testForwardPDF_data();
67
        void testGeneratorPreferences();
68
        void testSelectText();
69
        void testClickInternalLink();
70
        void testOpenUrlArguments();
71
        void test388288();
72 73
        void testSaveAs();
        void testSaveAs_data();
74 75 76
        void testSaveAsToNonExistingPath();
        void testSaveAsToSymlink();
        void testSaveIsSymlink();
77
        void testSidebarItemAfterSaving();
78
        void testViewModeSavingPerFile();
79 80
        void testSaveAsUndoStackAnnotations();
        void testSaveAsUndoStackAnnotations_data();
81
        void testSaveAsUndoStackForms();
82
        void testSaveAsUndoStackForms_data();
83 84 85 86 87 88 89 90 91 92
        void testMouseMoveOverLinkWhileInSelectionMode();
        void testClickUrlLinkWhileInSelectionMode();
        void testeTextSelectionOverAndAcrossLinks_data();
        void testeTextSelectionOverAndAcrossLinks();
        void testClickUrlLinkWhileLinkTextIsSelected();
        void testRClickWhileLinkTextIsSelected();
        void testRClickOverLinkWhileLinkTextIsSelected();
        void testRClickOnSelectionModeShoulShowFollowTheLinkMenu();
        void testClickAnywhereAfterSelectionShouldUnselect();
        void testeRectSelectionStartingOnLinks();
93
        void testCheckBoxReadOnly();
94
        void testCrashTextEditDestroy();
95
        void testAnnotWindow();
96
        void testAdditionalActionTriggers();
Tobias Deiminger's avatar
Tobias Deiminger committed
97
        void testTypewriterAnnotTool();
98
        void testJumpToPage();
99
        void testForwardBackwardNavigation();
100
        void testTabletProximityBehavior();
101
        void testOpenPrintPreview();
David Hurka's avatar
David Hurka committed
102
        void testMouseModeMenu();
103
        void testFullScreenRequest();
104 105 106

    private:
        void simulateMouseSelection(double startX, double startY, double endX, double endY, QWidget *target);
107 108
};

109 110
class PartThatHijacksQueryClose : public Okular::Part
{
111
    Q_OBJECT
112 113
    public:
        PartThatHijacksQueryClose(QWidget* parentWidget, QObject* parent,
114
                                  const QVariantList& args)
Vishesh Handa's avatar
Vishesh Handa committed
115
        : Okular::Part(parentWidget, parent, args),
116 117 118 119 120 121 122 123 124 125
          behavior(PassThru)
        {}

        enum Behavior { PassThru, ReturnTrue, ReturnFalse };

        void setQueryCloseBehavior(Behavior new_behavior)
        {
            behavior = new_behavior;
        }

126
        bool queryClose() override
127 128 129 130 131 132 133 134 135 136
        {
             if (behavior == PassThru)
                 return Okular::Part::queryClose();
             else // ReturnTrue or ReturnFalse
                 return (behavior == ReturnTrue);
        }
    private:
        Behavior behavior;
};

137

138 139 140 141 142 143
bool PartTest::openDocument(Okular::Part *part, const QString &filePath)
{
    part->openDocument( filePath );
    return part->m_document->isOpened();
}

144
// Test that Okular doesn't crash after a successful reload
145 146 147
void PartTest::testReload()
{
    QVariantList dummyArgs;
148
    Okular::Part part(nullptr, nullptr, dummyArgs);
149
    QVERIFY( openDocument(&part, QStringLiteral(KDESRCDIR "data/file1.pdf")) );
150 151 152 153
    part.reload();
    qApp->processEvents();
}

154 155 156 157
// Test that Okular doesn't crash after a canceled reload
void PartTest::testCanceledReload()
{
    QVariantList dummyArgs;
158
    PartThatHijacksQueryClose part(nullptr, nullptr, dummyArgs);
159
    QVERIFY( openDocument(&part, QStringLiteral(KDESRCDIR "data/file1.pdf")) );
160 161 162 163 164 165 166 167 168 169

    // When queryClose() returns false, the reload operation is canceled (as if
    // the user had chosen Cancel in the "Save changes?" message box)
    part.setQueryCloseBehavior(PartThatHijacksQueryClose::ReturnFalse);

    part.reload();

    qApp->processEvents();
}

170 171 172
void PartTest::testTOCReload()
{
    QVariantList dummyArgs;
173
    Okular::Part part(nullptr, nullptr, dummyArgs);
174
    QVERIFY( openDocument(&part, QStringLiteral(KDESRCDIR "data/tocreload.pdf")) );
175 176 177 178 179 180 181
    QCOMPARE(part.m_toc->expandedNodes().count(), 0);
    part.m_toc->m_treeView->expandAll();
    QCOMPARE(part.m_toc->expandedNodes().count(), 3);
    part.reload();
    qApp->processEvents();
    QCOMPARE(part.m_toc->expandedNodes().count(), 3);
}
182

rkflx's avatar
rkflx committed
183
void PartTest::testForwardPDF()
184 185 186 187
{
    QFETCH(QString, dir);

    QVariantList dummyArgs;
188
    Okular::Part part(nullptr, nullptr, dummyArgs);
189

190 191 192
    // Create temp dir named like this: ${system temp dir}/${random string}/${dir}
    const QTemporaryDir tempDir;
    const QDir workDir(QDir(tempDir.path()).filePath(dir));
Laurent Montel's avatar
Laurent Montel committed
193
    workDir.mkpath(QStringLiteral("."));
194

195 196
    QFile f(QStringLiteral(KDESRCDIR "data/synctextest.tex"));
    const QString texDestination = workDir.path() + QStringLiteral("/synctextest.tex");
197 198
    QVERIFY(f.copy(texDestination));
    QProcess process;
199
    process.setWorkingDirectory(workDir.path());
200

Laurent Montel's avatar
Laurent Montel committed
201
    const QString pdflatexPath(QStandardPaths::findExecutable(QStringLiteral("pdflatex")));
202 203 204 205
    if (pdflatexPath.isEmpty()) {
        QFAIL("pdflatex executable not found, but needed for the test. Try installing the respective TeXLive packages.");
    }
    process.start(pdflatexPath, QStringList() << QStringLiteral("-synctex=1") << QStringLiteral("-interaction=nonstopmode") << texDestination);
206 207 208 209 210 211 212 213
    bool started = process.waitForStarted();
    if (!started) {
        qDebug() << "start error:" << process.error();
        qDebug() << "start stdout:" << process.readAllStandardOutput();
        qDebug() << "start stderr:" << process.readAllStandardError();
    }
    QVERIFY(started);

214
    process.waitForFinished();
215 216 217 218 219
    if (process.exitStatus() != QProcess::NormalExit || process.exitCode() != 0) {
        qDebug() << "exit error:" << process.error() << "status" << process.exitStatus() << "code" << process.exitCode();
        qDebug() << "exit stdout:" << process.readAllStandardOutput();
        qDebug() << "exit stderr:" << process.readAllStandardError();
    }
220

221
    const QString pdfResult = workDir.path() + QStringLiteral("/synctextest.pdf");
222

223
    QVERIFY(QFile::exists(pdfResult));
224

225
    QVERIFY( openDocument(&part, pdfResult) );
226 227 228
    part.m_document->setViewportPage(0);
    QCOMPARE(part.m_document->currentPage(), 0u);
    part.closeUrl();
229

Lukáš Tinkl's avatar
Lukáš Tinkl committed
230
    QUrl u(QUrl::fromLocalFile(pdfResult));
231
    u.setFragment(QStringLiteral("src:100") + texDestination);
232 233
    part.openUrl(u);
    QCOMPARE(part.m_document->currentPage(), 1u);
234 235
}

rkflx's avatar
rkflx committed
236
void PartTest::testForwardPDF_data()
237 238 239
{
    QTest::addColumn<QString>("dir");

Laurent Montel's avatar
Laurent Montel committed
240
    QTest::newRow("non-utf8") << QStringLiteral("synctextest");
241 242
    //QStringliteral is broken on windows with non ascii chars so using QString::fromUtf8
    QTest::newRow("utf8")     << QString::fromUtf8("ßðđđŋßðđŋ");
243 244
}

245 246 247 248
void PartTest::testGeneratorPreferences()
{
    KConfigDialog * dialog;
    QVariantList dummyArgs;
249
    Okular::Part part(nullptr, nullptr, dummyArgs);
250 251 252 253 254 255 256 257 258 259 260 261 262

    // Test that we don't crash while opening the dialog
    dialog = part.slotGeneratorPreferences();
    qApp->processEvents();
    delete dialog; // closes the dialog and recursively destroys all widgets

    // Test that we don't crash while opening a new instance of the dialog
    // This catches attempts to reuse widgets that have been destroyed
    dialog = part.slotGeneratorPreferences();
    qApp->processEvents();
    delete dialog;
}

263 264 265
void PartTest::testSelectText()
{
    QVariantList dummyArgs;
266
    Okular::Part part(nullptr, nullptr, dummyArgs);
267
    QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/file2.pdf")));
268
    part.widget()->show();
Albert Astals Cid's avatar
Albert Astals Cid committed
269
    QVERIFY(QTest::qWaitForWindowExposed(part.widget()));
270

271 272
    part.m_document->setViewportPage(0);

273
    // wait for pixmap
274
    QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView));
275

276 277 278 279 280
    const int width = part.m_pageView->horizontalScrollBar()->maximum() +
                      part.m_pageView->viewport()->width();
    const int height = part.m_pageView->verticalScrollBar()->maximum() +
                       part.m_pageView->viewport()->height();

281
    QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect"));
282

Albert Astals Cid's avatar
Albert Astals Cid committed
283 284 285 286
    const int mouseY = height * 0.052;
    const int mouseStartX = width * 0.12;
    const int mouseEndX = width * 0.7;

287
    simulateMouseSelection(mouseStartX, mouseY, mouseEndX, mouseY, part.m_pageView->viewport());
288 289

    QApplication::clipboard()->clear();
290
    QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "copyTextSelection"));
291

292
    QCOMPARE(QApplication::clipboard()->text(), QStringLiteral("Hola que tal\n"));
293 294
}

295 296 297
void PartTest::testClickInternalLink()
{
    QVariantList dummyArgs;
298
    Okular::Part part(nullptr, nullptr, dummyArgs);
299
    QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/file2.pdf")));
300
    part.widget()->show();
Albert Astals Cid's avatar
Albert Astals Cid committed
301
    QVERIFY(QTest::qWaitForWindowExposed(part.widget()));
302 303 304 305

    part.m_document->setViewportPage(0);

    // wait for pixmap
306
    QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView));
307

308 309 310 311 312
    const int width = part.m_pageView->horizontalScrollBar()->maximum() +
                      part.m_pageView->viewport()->width();
    const int height = part.m_pageView->verticalScrollBar()->maximum() +
                       part.m_pageView->viewport()->height();

313 314 315
    QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseNormal");

    QCOMPARE(part.m_document->currentPage(), 0u);
316
    QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * 0.17, height * 0.05));
Albert Astals Cid's avatar
Albert Astals Cid committed
317
    QTest::mouseClick(part.m_pageView->viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(width * 0.17, height * 0.05));
318 319 320 321 322 323 324 325 326 327 328 329
    QTRY_COMPARE(part.m_document->currentPage(), 1u);
}

// cursor switches to Hand when hovering over link in TextSelect mode.
void PartTest::testMouseMoveOverLinkWhileInSelectionMode()
{
    QVariantList dummyArgs;
    Okular::Part part(nullptr, nullptr, dummyArgs);
    QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf")));
    // resize window to avoid problem with selection areas
    part.widget()->resize(800, 600);
    part.widget()->show();
Albert Astals Cid's avatar
Albert Astals Cid committed
330
    QVERIFY(QTest::qWaitForWindowExposed(part.widget()));
331 332 333 334 335 336

    part.m_document->setViewportPage(0);

    // wait for pixmap
    QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView));

337 338 339 340 341
    const int width = part.m_pageView->horizontalScrollBar()->maximum() +
                      part.m_pageView->viewport()->width();
    const int height = part.m_pageView->verticalScrollBar()->maximum() +
                       part.m_pageView->viewport()->height();

342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360
    // enter text-selection mode
    QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect"));

    // move mouse over link
    QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * 0.250, height * 0.127));

    // check if mouse icon changed to proper icon
    QTRY_COMPARE(part.m_pageView->cursor().shape(), Qt::PointingHandCursor);
}

// clicking on hyperlink jumps to destination in TextSelect mode.
void PartTest::testClickUrlLinkWhileInSelectionMode()
{
    QVariantList dummyArgs;
    Okular::Part part(nullptr, nullptr, dummyArgs);
    QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf")));
    // resize window to avoid problem with selection areas
    part.widget()->resize(800, 600);
    part.widget()->show();
Albert Astals Cid's avatar
Albert Astals Cid committed
361
    QVERIFY(QTest::qWaitForWindowExposed(part.widget()));
362 363 364 365 366 367

    part.m_document->setViewportPage(0);

    // wait for pixmap
    QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView));

368 369 370 371 372
    const int width = part.m_pageView->horizontalScrollBar()->maximum() +
                      part.m_pageView->viewport()->width();
    const int height = part.m_pageView->verticalScrollBar()->maximum() +
                       part.m_pageView->viewport()->height();

373 374 375 376
    // enter text-selection mode
    QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect"));

    // overwrite urlHandler for 'mailto' urls
Laurent Montel's avatar
Laurent Montel committed
377 378
    QDesktopServices::setUrlHandler(QStringLiteral("mailto"), this, "urlHandler");
    QSignalSpy openUrlSignalSpy(this, &PartTest::urlHandler);
379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396

    // click on url
    QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * 0.250, height * 0.127));
    QTest::mouseClick(part.m_pageView->viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(width * 0.250, height * 0.127));

    // expect that the urlHandler signal was called
    QTRY_COMPARE(openUrlSignalSpy.count(), 1);
    QList<QVariant> arguments = openUrlSignalSpy.takeFirst();
    QCOMPARE(arguments.at(0).value<QUrl>(), QUrl("mailto:foo@foo.bar"));
}

void PartTest::testeTextSelectionOverAndAcrossLinks_data()
{
    QTest::addColumn<double>("mouseStartX");
    QTest::addColumn<double>("mouseEndX");
    QTest::addColumn<QString>("expectedResult");

    // can text-select "over and across" hyperlink.
397
    QTest::newRow("start selection before link") << 0.1564 << 0.2943 << QStringLiteral(" a link: foo@foo.b");
398
    // can text-select starting at text and ending selection in middle of hyperlink.
399
    QTest::newRow("start selection in the middle of the link") << 0.28 << 0.382 << QStringLiteral("o.bar\n");
400
    // text selection works when selecting left to right or right to left
401
    QTest::newRow("start selection after link") << 0.40 << 0.05 << QStringLiteral("This is a link: foo@foo.bar\n");
402 403 404 405 406 407 408 409 410 411 412
}

// can text-select "over and across" hyperlink.
void PartTest::testeTextSelectionOverAndAcrossLinks()
{
    QVariantList dummyArgs;
    Okular::Part part(nullptr, nullptr, dummyArgs);
    QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf")));
    // resize window to avoid problem with selection areas
    part.widget()->resize(800, 600);
    part.widget()->show();
Albert Astals Cid's avatar
Albert Astals Cid committed
413
    QVERIFY(QTest::qWaitForWindowExposed(part.widget()));
414 415 416 417 418 419

    part.m_document->setViewportPage(0);

    // wait for pixmap
    QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView));

420 421 422 423 424
    const int width = part.m_pageView->horizontalScrollBar()->maximum() +
                      part.m_pageView->viewport()->width();
    const int height = part.m_pageView->verticalScrollBar()->maximum() +
                       part.m_pageView->viewport()->height();

425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452
    // enter text-selection mode
    QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect"));

    const double mouseY = height * 0.127;
    QFETCH(double, mouseStartX);
    QFETCH(double, mouseEndX);

    mouseStartX = width * mouseStartX;
    mouseEndX = width * mouseEndX;

    simulateMouseSelection(mouseStartX, mouseY, mouseEndX, mouseY, part.m_pageView->viewport());

    QApplication::clipboard()->clear();
    QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "copyTextSelection"));

    QFETCH(QString, expectedResult);
    QCOMPARE(QApplication::clipboard()->text(), expectedResult);
}

// can jump to link while there's an active selection of text.
void PartTest::testClickUrlLinkWhileLinkTextIsSelected()
{
    QVariantList dummyArgs;
    Okular::Part part(nullptr, nullptr, dummyArgs);
    QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf")));
    // resize window to avoid problem with selection areas
    part.widget()->resize(800, 600);
    part.widget()->show();
Albert Astals Cid's avatar
Albert Astals Cid committed
453
    QVERIFY(QTest::qWaitForWindowExposed(part.widget()));
454 455 456 457 458 459

    part.m_document->setViewportPage(0);

    // wait for pixmap
    QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView));

460 461 462 463 464
    const int width = part.m_pageView->horizontalScrollBar()->maximum() +
                      part.m_pageView->viewport()->width();
    const int height = part.m_pageView->verticalScrollBar()->maximum() +
                       part.m_pageView->viewport()->height();

465 466 467 468 469 470 471 472 473 474
    // enter text-selection mode
    QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect"));

    const double mouseY = height * 0.127;
    const double mouseStartX = width * 0.13;
    const double mouseEndX = width * 0.40;

    simulateMouseSelection(mouseStartX, mouseY, mouseEndX, mouseY, part.m_pageView->viewport());

    // overwrite urlHandler for 'mailto' urls
Laurent Montel's avatar
Laurent Montel committed
475 476
    QDesktopServices::setUrlHandler(QStringLiteral("mailto"), this, "urlHandler");
    QSignalSpy openUrlSignalSpy(this, &PartTest::urlHandler);
477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499

    // click on url
    const double mouseClickX = width * 0.2997;
    const double mouseClickY = height * 0.1293;

    QTest::mouseMove(part.m_pageView->viewport(), QPoint(mouseClickX, mouseClickY));
    QTest::mouseClick(part.m_pageView->viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(mouseClickX, mouseClickY), 1000);

    // expect that the urlHandler signal was called
    QTRY_COMPARE(openUrlSignalSpy.count(), 1);
    QList<QVariant> arguments = openUrlSignalSpy.takeFirst();
    QCOMPARE(arguments.at(0).value<QUrl>(), QUrl("mailto:foo@foo.bar"));
}

// r-click on the selected text gives the "Go To:" content menu option
void PartTest::testRClickWhileLinkTextIsSelected()
{
    QVariantList dummyArgs;
    Okular::Part part(nullptr, nullptr, dummyArgs);
    QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf")));
    // resize window to avoid problem with selection areas
    part.widget()->resize(800, 600);
    part.widget()->show();
Albert Astals Cid's avatar
Albert Astals Cid committed
500
    QVERIFY(QTest::qWaitForWindowExposed(part.widget()));
501 502 503 504 505 506

    part.m_document->setViewportPage(0);

    // wait for pixmap
    QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView));

507 508 509 510 511
    const int width = part.m_pageView->horizontalScrollBar()->maximum() +
                      part.m_pageView->viewport()->width();
    const int height = part.m_pageView->verticalScrollBar()->maximum() +
                       part.m_pageView->viewport()->height();

512 513 514 515 516 517 518 519 520 521 522 523
    // enter text-selection mode
    QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect"));

    const double mouseY = height * 0.162;
    const double mouseStartX = width * 0.42;
    const double mouseEndX = width * 0.60;

    simulateMouseSelection(mouseStartX, mouseY, mouseEndX, mouseY, part.m_pageView->viewport());

    // Need to do this because the pop-menu will have his own mainloop and will block tests until
    // the menu disappear
    PageView *view = part.m_pageView;
Albert Astals Cid's avatar
Albert Astals Cid committed
524
    bool menuClosed = false;
525
    QTimer::singleShot(2000, view, [view, &menuClosed]() {
526
        // check if popup menu is active and visible
Laurent Montel's avatar
Laurent Montel committed
527
        QMenu *menu = qobject_cast<QMenu*>(view->findChild<QMenu*>(QStringLiteral("PopupMenu")));
528 529 530 531
        QVERIFY(menu);
        QVERIFY(menu->isVisible());

        // check if the menu contains go-to link action
Laurent Montel's avatar
Laurent Montel committed
532
        QAction *goToAction = qobject_cast<QAction*>(menu->findChild<QAction*>(QStringLiteral("GoToAction")));
533 534 535
        QVERIFY(goToAction);

        // check if the "follow this link" action is not visible
Laurent Montel's avatar
Laurent Montel committed
536
        QAction *processLinkAction = qobject_cast<QAction*>(menu->findChild<QAction*>(QStringLiteral("ProcessLinkAction")));
537 538 539
        QVERIFY(!processLinkAction);

        // check if the "copy link address" action is not visible
Laurent Montel's avatar
Laurent Montel committed
540
        QAction *copyLinkLocation = qobject_cast<QAction*>(menu->findChild<QAction*>(QStringLiteral("CopyLinkLocationAction")));
541 542 543 544
        QVERIFY(!copyLinkLocation);

        // close menu to continue test
        menu->close();
Albert Astals Cid's avatar
Albert Astals Cid committed
545
        menuClosed = true;
546 547 548 549 550 551 552 553 554 555
    });

    // click on url
    const double mouseClickX = width * 0.425;
    const double mouseClickY = height * 0.162;

    QTest::mouseMove(part.m_pageView->viewport(), QPoint(mouseClickX, mouseClickY));
    QTest::mouseClick(part.m_pageView->viewport(), Qt::RightButton, Qt::NoModifier, QPoint(mouseClickX, mouseClickY), 1000);

    // will continue after pop-menu get closed
Albert Astals Cid's avatar
Albert Astals Cid committed
556
    QTRY_VERIFY(menuClosed);
557 558 559 560 561 562 563 564 565 566 567 568
}


// r-click on the link gives the "follow this link" content menu option
void PartTest::testRClickOverLinkWhileLinkTextIsSelected()
{
    QVariantList dummyArgs;
    Okular::Part part(nullptr, nullptr, dummyArgs);
    QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf")));
    // resize window to avoid problem with selection areas
    part.widget()->resize(800, 600);
    part.widget()->show();
Albert Astals Cid's avatar
Albert Astals Cid committed
569
    QVERIFY(QTest::qWaitForWindowExposed(part.widget()));
570 571 572 573 574 575

    part.m_document->setViewportPage(0);

    // wait for pixmap
    QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView));

576 577 578 579 580
    const int width = part.m_pageView->horizontalScrollBar()->maximum() +
                      part.m_pageView->viewport()->width();
    const int height = part.m_pageView->verticalScrollBar()->maximum() +
                       part.m_pageView->viewport()->height();

581 582 583 584 585 586 587 588 589 590 591 592
    // enter text-selection mode
    QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect"));

    const double mouseY = height * 0.162;
    const double mouseStartX = width * 0.42;
    const double mouseEndX = width * 0.60;

    simulateMouseSelection(mouseStartX, mouseY, mouseEndX, mouseY, part.m_pageView->viewport());

    // Need to do this because the pop-menu will have his own mainloop and will block tests until
    // the menu disappear
    PageView *view = part.m_pageView;
Albert Astals Cid's avatar
Albert Astals Cid committed
593
    bool menuClosed = false;
594
    QTimer::singleShot(2000, view, [view, &menuClosed]() {
595
        // check if popup menu is active and visible
Laurent Montel's avatar
Laurent Montel committed
596
        QMenu *menu = qobject_cast<QMenu*>(view->findChild<QMenu*>(QStringLiteral("PopupMenu")));
597 598 599 600
        QVERIFY(menu);
        QVERIFY(menu->isVisible());

        // check if the menu contains "follow this link" action
Laurent Montel's avatar
Laurent Montel committed
601
        QAction *processLinkAction = qobject_cast<QAction*>(menu->findChild<QAction*>(QStringLiteral("ProcessLinkAction")));
602 603 604
        QVERIFY(processLinkAction);

        // check if the menu contains "copy link address" action
Laurent Montel's avatar
Laurent Montel committed
605
        QAction *copyLinkLocation = qobject_cast<QAction*>(menu->findChild<QAction*>(QStringLiteral("CopyLinkLocationAction")));
606 607 608 609
        QVERIFY(copyLinkLocation);

        // close menu to continue test
        menu->close();
Albert Astals Cid's avatar
Albert Astals Cid committed
610
        menuClosed = true;
611 612 613 614 615 616 617 618 619 620
    });

    // click on url
    const double mouseClickX = width * 0.593;
    const double mouseClickY = height * 0.162;

    QTest::mouseMove(part.m_pageView->viewport(), QPoint(mouseClickX, mouseClickY));
    QTest::mouseClick(part.m_pageView->viewport(), Qt::RightButton, Qt::NoModifier, QPoint(mouseClickX, mouseClickY), 1000);

    // will continue after pop-menu get closed
Albert Astals Cid's avatar
Albert Astals Cid committed
621
    QTRY_VERIFY(menuClosed);
622 623 624 625 626 627 628 629 630 631
}

void PartTest::testRClickOnSelectionModeShoulShowFollowTheLinkMenu()
{
    QVariantList dummyArgs;
    Okular::Part part(nullptr, nullptr, dummyArgs);
    QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf")));
    // resize window to avoid problem with selection areas
    part.widget()->resize(800, 600);
    part.widget()->show();
Albert Astals Cid's avatar
Albert Astals Cid committed
632
    QVERIFY(QTest::qWaitForWindowExposed(part.widget()));
633 634 635 636 637 638

    part.m_document->setViewportPage(0);

    // wait for pixmap
    QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView));

639 640 641 642 643
    const int width = part.m_pageView->horizontalScrollBar()->maximum() +
                      part.m_pageView->viewport()->width();
    const int height = part.m_pageView->verticalScrollBar()->maximum() +
                       part.m_pageView->viewport()->height();

644 645 646 647 648 649
    // enter text-selection mode
    QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect"));

    // Need to do this because the pop-menu will have his own mainloop and will block tests until
    // the menu disappear
    PageView *view = part.m_pageView;
Albert Astals Cid's avatar
Albert Astals Cid committed
650
    bool menuClosed = false;
651
    QTimer::singleShot(2000, view, [view, &menuClosed]() {
652
        // check if popup menu is active and visible
Laurent Montel's avatar
Laurent Montel committed
653
        QMenu *menu = qobject_cast<QMenu*>(view->findChild<QMenu*>(QStringLiteral("PopupMenu")));
654 655 656 657
        QVERIFY(menu);
        QVERIFY(menu->isVisible());

        // check if the menu contains "Follow this link" action
Laurent Montel's avatar
Laurent Montel committed
658
        QAction *processLink = qobject_cast<QAction*>(menu->findChild<QAction*>(QStringLiteral("ProcessLinkAction")));
659 660 661
        QVERIFY(processLink);

        // chek if the menu contains  "Copy Link Address" action
Laurent Montel's avatar
Laurent Montel committed
662
        QAction *actCopyLinkLocation = qobject_cast<QAction*>(menu->findChild<QAction*>(QStringLiteral("CopyLinkLocationAction")));
663 664 665 666
        QVERIFY(actCopyLinkLocation);

        // close menu to continue test
        menu->close();
Albert Astals Cid's avatar
Albert Astals Cid committed
667
        menuClosed = true;
668 669 670 671 672 673 674 675 676 677
    });

    // r-click on url
    const double mouseClickX = width * 0.604;
    const double mouseClickY = height * 0.162;

    QTest::mouseMove(part.m_pageView->viewport(), QPoint(mouseClickX, mouseClickY));
    QTest::mouseClick(part.m_pageView->viewport(), Qt::RightButton, Qt::NoModifier, QPoint(mouseClickX, mouseClickY), 1000);

    // will continue after pop-menu get closed
Albert Astals Cid's avatar
Albert Astals Cid committed
678
    QTRY_VERIFY(menuClosed);
679 680 681 682 683 684 685 686 687 688
}

void PartTest::testClickAnywhereAfterSelectionShouldUnselect()
{
    QVariantList dummyArgs;
    Okular::Part part(nullptr, nullptr, dummyArgs);
    QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf")));
    // resize window to avoid problem with selection areas
    part.widget()->resize(800, 600);
    part.widget()->show();
Albert Astals Cid's avatar
Albert Astals Cid committed
689
    QVERIFY(QTest::qWaitForWindowExposed(part.widget()));
690 691 692 693 694 695

    part.m_document->setViewportPage(0);

    // wait for pixmap
    QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView));

696 697 698 699 700
    const int width = part.m_pageView->horizontalScrollBar()->maximum() +
                      part.m_pageView->viewport()->width();
    const int height = part.m_pageView->verticalScrollBar()->maximum() +
                       part.m_pageView->viewport()->height();

701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727
    // enter text-selection mode
    QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect"));

    const double mouseY = height * 0.162;
    const double mouseStartX = width * 0.42;
    const double mouseEndX = width * 0.60;

    simulateMouseSelection(mouseStartX, mouseY, mouseEndX, mouseY, part.m_pageView->viewport());

    // click on url
    const double mouseClickX = width * 0.10;

    QTest::mouseMove(part.m_pageView->viewport(), QPoint(mouseClickX, mouseY));
    QTest::mouseClick(part.m_pageView->viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(mouseClickX, mouseY), 1000);

    QApplication::clipboard()->clear();
    QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "copyTextSelection"));

    // check if copied text is empty what means no text selected
    QVERIFY(QApplication::clipboard()->text().isEmpty());
}

void PartTest::testeRectSelectionStartingOnLinks()
{
    QVariantList dummyArgs;
    Okular::Part part(nullptr, nullptr, dummyArgs);
    QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf")));
728 729
    // hide info messages as they interfere with selection area
    Okular::Settings::self()->setShowOSD(false);;
730
    part.widget()->show();
Albert Astals Cid's avatar
Albert Astals Cid committed
731
    QVERIFY(QTest::qWaitForWindowExposed(part.widget()));
732 733 734 735 736 737

    part.m_document->setViewportPage(0);

    // wait for pixmap
    QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView));

738 739 740 741 742
    const int width = part.m_pageView->horizontalScrollBar()->maximum() +
                      part.m_pageView->viewport()->width();
    const int height = part.m_pageView->verticalScrollBar()->maximum() +
                       part.m_pageView->viewport()->height();

743 744 745 746 747 748 749 750 751 752 753
    // enter text-selection mode
    QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseSelect"));

    const double mouseStartY = height * 0.127;
    const double mouseEndY = height * 0.127;
    const double mouseStartX = width *  0.28;
    const double mouseEndX = width *  0.382;

    // Need to do this because the pop-menu will have his own mainloop and will block tests until
    // the menu disappear
    PageView *view = part.m_pageView;
Albert Astals Cid's avatar
Albert Astals Cid committed
754
    bool menuClosed = false;
755
    QTimer::singleShot(2000, view, [view, &menuClosed]() {
756 757 758
        QApplication::clipboard()->clear();

        // check if popup menu is active and visible
Laurent Montel's avatar
Laurent Montel committed
759
        QMenu *menu = qobject_cast<QMenu*>(view->findChild<QMenu*>(QStringLiteral("PopupMenu")));
760 761 762 763
        QVERIFY(menu);
        QVERIFY(menu->isVisible());

        // check if the copy selected text to clipboard is present
Laurent Montel's avatar
Laurent Montel committed
764
        QAction *copyAct = qobject_cast<QAction*>(menu->findChild<QAction*>(QStringLiteral("CopyTextToClipboard")));
765 766 767
        QVERIFY(copyAct);

        menu->close();
Albert Astals Cid's avatar
Albert Astals Cid committed
768
        menuClosed = true;
769 770 771 772 773
    });

    simulateMouseSelection(mouseStartX, mouseStartY, mouseEndX, mouseEndY, part.m_pageView->viewport());

    // wait menu get closed
Albert Astals Cid's avatar
Albert Astals Cid committed
774
    QTRY_VERIFY(menuClosed);
775 776 777 778 779
}


void PartTest::simulateMouseSelection(double startX, double startY, double endX, double endY, QWidget *target)
{
780 781 782 783 784 785
    const int steps = 5;
    const double diffX = endX - startX;
    const double diffY = endY - startY;
    const double diffXStep = diffX / steps;
    const double diffYStep = diffY / steps;

786 787 788
    QTestEventList events;
    events.addMouseMove(QPoint(startX, startY));
    events.addMousePress(Qt::LeftButton, Qt::NoModifier, QPoint(startX, startY));
789 790 791 792
    for (int i = 0; i < steps - 1; ++i) {
        events.addMouseMove(QPoint(startX + i * diffXStep, startY + i * diffYStep));
        events.addDelay(100);
    }
793
    events.addMouseMove(QPoint(endX, endY));
794
    events.addDelay(100);
795 796 797
    events.addMouseRelease(Qt::LeftButton, Qt::NoModifier, QPoint(endX, endY));

    events.simulate(target);
798 799
}

800 801 802 803 804 805 806
void PartTest::testSaveAsToNonExistingPath()
{
    Okular::Part part(nullptr, nullptr, QVariantList());
    part.openDocument( KDESRCDIR "data/file1.pdf" );

    QString saveFilePath;
    {
Laurent Montel's avatar
Laurent Montel committed
807
        QTemporaryFile saveFile( QStringLiteral( "%1/okrXXXXXX.pdf" ).arg( QDir::tempPath() ) );
808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825
        saveFile.open();
        saveFilePath = saveFile.fileName();
        // QTemporaryFile is destroyed and the file it created is gone, this is a TOCTOU but who cares
    }

    QVERIFY( !QFileInfo::exists( saveFilePath ) );

    QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFilePath ), Part::NoSaveAsFlags ) );

    QFile::remove( saveFilePath );
}

void PartTest::testSaveAsToSymlink()
{
#ifdef Q_OS_UNIX
    Okular::Part part(nullptr, nullptr, QVariantList());
    part.openDocument( KDESRCDIR "data/file1.pdf" );

Laurent Montel's avatar
Laurent Montel committed
826
    QTemporaryFile newFile( QStringLiteral( "%1/okrXXXXXX.pdf" ).arg( QDir::tempPath() ) );
827 828 829 830
    newFile.open();

    QString linkFilePath;
    {
Laurent Montel's avatar
Laurent Montel committed
831
        QTemporaryFile linkFile( QStringLiteral( "%1/okrXXXXXX.pdf" ).arg( QDir::tempPath() ) );
832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855
        linkFile.open();
        linkFilePath = linkFile.fileName();
        // QTemporaryFile is destroyed and the file it created is gone, this is a TOCTOU but who cares
    }

    QFile::link( newFile.fileName(), linkFilePath );

    QVERIFY( QFileInfo( linkFilePath ).isSymLink() );

    QVERIFY( part.saveAs( QUrl::fromLocalFile( linkFilePath ), Part::NoSaveAsFlags ) );

    QVERIFY( QFileInfo( linkFilePath ).isSymLink() );

    QFile::remove( linkFilePath );
#endif
}

void PartTest::testSaveIsSymlink()
{
#ifdef Q_OS_UNIX
    Okular::Part part(nullptr, nullptr, QVariantList());

    QString newFilePath;
    {
Laurent Montel's avatar
Laurent Montel committed
856
        QTemporaryFile newFile( QStringLiteral( "%1/okrXXXXXX.pdf" ).arg( QDir::tempPath() ) );
857 858 859 860 861 862 863 864 865
        newFile.open();
        newFilePath = newFile.fileName();
        // QTemporaryFile is destroyed and the file it created is gone, this is a TOCTOU but who cares
    }

    QFile::copy( KDESRCDIR "data/file1.pdf", newFilePath );

    QString linkFilePath;
    {
Laurent Montel's avatar
Laurent Montel committed
866
        QTemporaryFile linkFile( QStringLiteral( "%1/okrXXXXXX.pdf" ).arg( QDir::tempPath() ) );
867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885
        linkFile.open();
        linkFilePath = linkFile.fileName();
        // QTemporaryFile is destroyed and the file it created is gone, this is a TOCTOU but who cares
    }

    QFile::link( newFilePath, linkFilePath );

    QVERIFY( QFileInfo( linkFilePath ).isSymLink() );

    part.openDocument( linkFilePath );
    QVERIFY( part.saveAs( QUrl::fromLocalFile( linkFilePath ), Part::NoSaveAsFlags ) );

    QVERIFY( QFileInfo( linkFilePath ).isSymLink() );

    QFile::remove( newFilePath );
    QFile::remove( linkFilePath );
#endif
}

886 887 888 889 890
void PartTest::testSaveAs()
{
    QFETCH(QString, file);
    QFETCH(QString, extension);
    QFETCH(bool, nativelySupportsAnnotations);
891
    QFETCH(bool, canSwapBackingFile);
892

893
    QScopedPointer<TestingUtils::CloseDialogHelper> closeDialogHelper;
894 895

    QString annotName;
Laurent Montel's avatar
Laurent Montel committed
896
    QTemporaryFile archiveSave( QStringLiteral( "%1/okrXXXXXX.okular" ).arg( QDir::tempPath() ) );
897 898
    QTemporaryFile nativeDirectSave( QStringLiteral( "%1/okrXXXXXX.%2" ).arg( QDir::tempPath(), extension ) );
    QTemporaryFile nativeFromArchiveFile( QStringLiteral( "%1/okrXXXXXX.%2" ).arg( QDir::tempPath(), extension ) );
899 900 901 902 903 904
    QVERIFY( archiveSave.open() );
    archiveSave.close();
    QVERIFY( nativeDirectSave.open() );
    nativeDirectSave.close();
    QVERIFY( nativeFromArchiveFile.open() );
    nativeFromArchiveFile.close();
905 906 907

    qDebug() << "Open file, add annotation and save both natively and to .okular";
    {
908
        Okular::Part part(nullptr, nullptr, QVariantList());
909
        part.openDocument( file );
910
        part.m_document->documentInfo();
911

912 913
        QCOMPARE(part.m_document->canSwapBackingFile(), canSwapBackingFile);

914 915
        Okular::Annotation *annot = new Okular::TextAnnotation();
        annot->setBoundingRectangle( Okular::NormalizedRect( 0.1, 0.1, 0.15, 0.15 ) );
Laurent Montel's avatar
Laurent Montel committed
916
        annot->setContents( QStringLiteral("annot contents") );
917 918 919
        part.m_document->addPageAnnotation( 0, annot );
        annotName = annot->uniqueName();

920 921 922 923
        if ( canSwapBackingFile )
        {
            if ( !nativelySupportsAnnotations )
            {
924
                closeDialogHelper.reset(new TestingUtils::CloseDialogHelper( &part, QDialogButtonBox::No  )); // this is the "you're going to lose the annotations" dialog
925 926 927 928 929 930 931 932 933 934 935 936
            }
            QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), Part::NoSaveAsFlags ) );
            // For backends that don't support annotations natively we mark the part as still modified
            // after a save because we keep the annotation around but it will get lost if the user closes the app
            // so we want to give her a last chance to save on close with the "you have changes dialog"
            QCOMPARE( part.isModified(), !nativelySupportsAnnotations );
            QVERIFY( part.saveAs( QUrl::fromLocalFile( archiveSave.fileName() ), Part::SaveAsOkularArchive ) );
        }
        else
        {
            // We need to save to archive first otherwise we lose the annotation

937
            closeDialogHelper.reset(new TestingUtils::CloseDialogHelper( &part, QDialogButtonBox::Yes )); // this is the "you're going to lose the undo/redo stack" dialog
938 939 940 941
            QVERIFY( part.saveAs( QUrl::fromLocalFile( archiveSave.fileName() ), Part::SaveAsOkularArchive ) );

            if ( !nativelySupportsAnnotations )
            {
942
                closeDialogHelper.reset(new TestingUtils::CloseDialogHelper( &part, QDialogButtonBox::No  )); // this is the "you're going to lose the annotations" dialog
943 944 945
            }
            QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), Part::NoSaveAsFlags ) );
        }
946

947
        QCOMPARE( part.m_document->documentInfo().get( Okular::DocumentInfo::FilePath ), part.m_document->currentDocument().toDisplayString() );
948 949 950 951 952
        part.closeUrl();
    }

    qDebug() << "Open the .okular, check that the annotation is present and save to native";
    {
953
        Okular::Part part(nullptr, nullptr, QVariantList());
954
        part.openDocument( archiveSave.fileName() );
955
        part.m_document->documentInfo();
956

957 958
        QCOMPARE( part.m_document->page( 0 )->annotations().size(), 1 );
        QCOMPARE( part.m_document->page( 0 )->annotations().first()->uniqueName(), annotName );
959

960 961
        if ( !nativelySupportsAnnotations )
        {
962
            closeDialogHelper.reset(new TestingUtils::CloseDialogHelper( &part, QDialogButtonBox::No  )); // this is the "you're going to lose the annotations" dialog
963 964 965 966 967 968 969 970
        }
        QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeFromArchiveFile.fileName() ), Part::NoSaveAsFlags ) );

        if ( canSwapBackingFile && !nativelySupportsAnnotations )
        {
            // For backends that don't support annotations natively we mark the part as still modified
            // after a save because we keep the annotation around but it will get lost if the user closes the app
            // so we want to give her a last chance to save on close with the "you have changes dialog"
971
            closeDialogHelper.reset(new TestingUtils::CloseDialogHelper( &part, QDialogButtonBox::No  )); // this is the "do you want to save or discard" dialog
972
        }
973

974
        QCOMPARE( part.m_document->documentInfo().get( Okular::DocumentInfo::FilePath ), part.m_document->currentDocument().toDisplayString() );
975 976 977 978 979 980
        part.closeUrl();
    }

    qDebug() << "Open the native file saved directly, and check that the annot"
        << "is there iff we expect it";
    {