File indexing completed on 2024-05-12 16:06:02

0001 /*
0002     SPDX-FileCopyrightText: 2019 João Netto <joaonetto901@gmail.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include <QAction>
0008 #include <QJsonArray>
0009 #include <QJsonDocument>
0010 #include <QMenu>
0011 #include <QTest>
0012 #include <QTimer>
0013 
0014 #include "../settings_core.h"
0015 #include "core/action.h"
0016 #include "core/document.h"
0017 #include "core/scripter.h"
0018 #include <QAbstractButton>
0019 #include <QAbstractItemModel>
0020 #include <QMap>
0021 #include <QMessageBox>
0022 #include <QMimeDatabase>
0023 #include <QMimeType>
0024 #include <QThread>
0025 #include <core/form.h>
0026 #include <core/page.h>
0027 #include <core/script/event_p.h>
0028 
0029 class MessageBoxHelper : public QObject
0030 {
0031     Q_OBJECT
0032 
0033 public:
0034     MessageBoxHelper(QMessageBox::StandardButton b, QString message, QMessageBox::Icon icon, QString title, bool hasCheckBox)
0035         : m_button(b)
0036         , m_clicked(false)
0037         , m_message(std::move(message))
0038         , m_icon(icon)
0039         , m_title(std::move(title))
0040         , m_checkBox(hasCheckBox)
0041     {
0042         QTimer::singleShot(0, this, &MessageBoxHelper::closeMessageBox);
0043     }
0044 
0045     ~MessageBoxHelper() override
0046     {
0047         QVERIFY(m_clicked);
0048     }
0049 
0050 private Q_SLOTS:
0051     void closeMessageBox()
0052     {
0053         const QWidgetList allToplevelWidgets = QApplication::topLevelWidgets();
0054         QMessageBox *mb = nullptr;
0055         for (QWidget *w : allToplevelWidgets) {
0056             if (w->inherits("QMessageBox")) {
0057                 mb = qobject_cast<QMessageBox *>(w);
0058                 QCOMPARE(mb->text(), m_message);
0059                 QCOMPARE(mb->windowTitle(), m_title);
0060                 QCOMPARE(mb->icon(), m_icon);
0061                 QCheckBox *box = mb->checkBox();
0062                 QCOMPARE(box != nullptr, m_checkBox);
0063                 mb->button(m_button)->click();
0064             }
0065         }
0066         if (!mb) {
0067             QTimer::singleShot(0, this, &MessageBoxHelper::closeMessageBox);
0068             return;
0069         }
0070         m_clicked = true;
0071     }
0072 
0073 private:
0074     QMessageBox::StandardButton m_button;
0075     bool m_clicked;
0076     QString m_message;
0077     QMessageBox::Icon m_icon;
0078     QString m_title;
0079     bool m_checkBox;
0080 };
0081 
0082 class JSFunctionsTest : public QObject
0083 {
0084     Q_OBJECT
0085 
0086 private Q_SLOTS:
0087     void initTestCase();
0088     void testNthFieldName();
0089     void testDisplay();
0090     void testSetClearInterval();
0091     void testSetClearTimeOut();
0092     void testGetOCGs();
0093     void cleanupTestCase();
0094     void testAlert();
0095     void testPopUpMenu();
0096     void testPopUpMenuEx();
0097     void testPrintD();
0098     void testPrintD_data();
0099 
0100 private:
0101     Okular::Document *m_document;
0102     QMap<QString, Okular::FormField *> m_fields;
0103 };
0104 
0105 void JSFunctionsTest::initTestCase()
0106 {
0107     Okular::SettingsCore::instance(QStringLiteral("jsfunctionstest"));
0108     m_document = new Okular::Document(nullptr);
0109 
0110     const QString testFile = QStringLiteral(KDESRCDIR "data/kjsfunctionstest.pdf");
0111     QMimeDatabase db;
0112     const QMimeType mime = db.mimeTypeForFile(testFile);
0113     QCOMPARE(m_document->openDocument(testFile, QUrl(), mime), Okular::Document::OpenSuccess);
0114 
0115     const Okular::Page *page = m_document->page(0);
0116     const QList<Okular::FormField *> pageFormFields = page->formFields();
0117     for (Okular::FormField *ff : pageFormFields) {
0118         m_fields.insert(ff->name(), ff);
0119     }
0120 }
0121 
0122 void JSFunctionsTest::testNthFieldName()
0123 {
0124     for (int i = 0; i < 21; ++i) {
0125         Okular::ScriptAction *action = new Okular::ScriptAction(Okular::JavaScript,
0126                                                                 QStringLiteral("var field = Doc.getField( Doc.getNthFieldName(%1) );\
0127                                                                               field.display = display.visible;")
0128                                                                     .arg(i));
0129         m_document->processAction(action);
0130         QVERIFY(m_fields[QStringLiteral("0.%1").arg(i)]->isVisible());
0131         m_fields[QStringLiteral("0.%1").arg(i)]->setVisible(false);
0132         delete action;
0133     }
0134 }
0135 
0136 void JSFunctionsTest::testDisplay()
0137 {
0138     Okular::ScriptAction *action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("field = Doc.getField(\"0.0\");field.display=display.hidden;\
0139         field = Doc.getField(\"0.10\");field.display=display.visible;"));
0140     m_document->processAction(action);
0141     QVERIFY(!m_fields[QStringLiteral("0.0")]->isVisible());
0142     QVERIFY(!m_fields[QStringLiteral("0.0")]->isPrintable());
0143     QVERIFY(m_fields[QStringLiteral("0.10")]->isVisible());
0144     QVERIFY(m_fields[QStringLiteral("0.10")]->isPrintable());
0145     delete action;
0146 
0147     action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("field = Doc.getField(\"0.10\");field.display=display.noView;\
0148         field = Doc.getField(\"0.15\");field.display=display.noPrint;"));
0149     m_document->processAction(action);
0150     QVERIFY(!m_fields[QStringLiteral("0.10")]->isVisible());
0151     QVERIFY(m_fields[QStringLiteral("0.10")]->isPrintable());
0152     QVERIFY(m_fields[QStringLiteral("0.15")]->isVisible());
0153     QVERIFY(!m_fields[QStringLiteral("0.15")]->isPrintable());
0154     delete action;
0155 
0156     action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("field = Doc.getField(\"0.15\");field.display=display.hidden;\
0157         field = Doc.getField(\"0.20\");field.display=display.visible;"));
0158     m_document->processAction(action);
0159     QVERIFY(!m_fields[QStringLiteral("0.15")]->isVisible());
0160     QVERIFY(!m_fields[QStringLiteral("0.15")]->isPrintable());
0161     QVERIFY(m_fields[QStringLiteral("0.20")]->isVisible());
0162     QVERIFY(m_fields[QStringLiteral("0.20")]->isPrintable());
0163     delete action;
0164 
0165     action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("field = Doc.getField(\"0.20\");field.display=display.hidden;\
0166         field = Doc.getField(\"0.0\");field.display=display.visible;"));
0167     m_document->processAction(action);
0168     QVERIFY(!m_fields[QStringLiteral("0.20")]->isVisible());
0169     QVERIFY(!m_fields[QStringLiteral("0.20")]->isPrintable());
0170     QVERIFY(m_fields[QStringLiteral("0.0")]->isVisible());
0171     QVERIFY(m_fields[QStringLiteral("0.0")]->isPrintable());
0172     delete action;
0173 }
0174 
0175 void delay()
0176 {
0177     QTime dieTime = QTime::currentTime().addSecs(2);
0178     while (QTime::currentTime() < dieTime) {
0179         QCoreApplication::processEvents(QEventLoop::AllEvents, 100);
0180     }
0181 }
0182 
0183 void JSFunctionsTest::testSetClearInterval()
0184 {
0185     Okular::ScriptAction *action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("obj = new Object();obj.idx=0;\
0186         obj.inc=function(){field = Doc.getField(Doc.getNthFieldName(obj.idx));\
0187         field.display = display.visible;\
0188         obj.idx = obj.idx + 1;};\
0189         intv = app.setInterval('obj.inc()', 450);obj.idx;"));
0190     m_document->processAction(action);
0191     QVERIFY(m_fields[QStringLiteral("0.0")]->isVisible());
0192     QVERIFY(!m_fields[QStringLiteral("0.3")]->isVisible());
0193     delete action;
0194     delay();
0195 
0196     action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("app.clearInterval(intv);obj.idx;"));
0197     m_document->processAction(action);
0198     QVERIFY(m_fields[QStringLiteral("0.3")]->isVisible());
0199     delete action;
0200 }
0201 
0202 void JSFunctionsTest::testSetClearTimeOut()
0203 {
0204     Okular::ScriptAction *action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("intv = app.setTimeOut('obj.inc()', 1);obj.idx;"));
0205     m_document->processAction(action);
0206     QVERIFY(m_fields[QStringLiteral("0.3")]->isVisible());
0207     QVERIFY(!m_fields[QStringLiteral("0.4")]->isVisible());
0208     delay();
0209     delete action;
0210 
0211     QVERIFY(m_fields[QStringLiteral("0.4")]->isVisible());
0212 
0213     action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("intv = app.setTimeOut('obj.inc()', 2000);obj.idx;"));
0214     m_document->processAction(action);
0215     QVERIFY(m_fields[QStringLiteral("0.4")]->isVisible());
0216     delete action;
0217 
0218     action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("app.clearTimeOut(intv);obj.idx;"));
0219     m_document->processAction(action);
0220     QVERIFY(m_fields[QStringLiteral("0.4")]->isVisible());
0221     delay();
0222     QVERIFY(m_fields[QStringLiteral("0.4")]->isVisible());
0223     delete action;
0224 }
0225 
0226 void JSFunctionsTest::testGetOCGs()
0227 {
0228     QAbstractItemModel *model = m_document->layersModel();
0229 
0230     Okular::ScriptAction *action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("var ocg = this.getOCGs(this.pageNum);\
0231         ocg[0].state = false;"));
0232     m_document->processAction(action);
0233     QVERIFY(!model->data(model->index(0, 0), Qt::CheckStateRole).toBool());
0234     delete action;
0235 
0236     action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("ocg[0].state = true;"));
0237     m_document->processAction(action);
0238     QVERIFY(model->data(model->index(0, 0), Qt::CheckStateRole).toBool());
0239     delete action;
0240 
0241     action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("ocg[1].state = false;"));
0242     m_document->processAction(action);
0243     QVERIFY(!model->data(model->index(1, 0), Qt::CheckStateRole).toBool());
0244     delete action;
0245 
0246     action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("ocg[1].state = true;"));
0247     m_document->processAction(action);
0248     QVERIFY(model->data(model->index(1, 0), Qt::CheckStateRole).toBool());
0249     delete action;
0250 
0251     action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("ocg[2].state = false;"));
0252     m_document->processAction(action);
0253     QVERIFY(!model->data(model->index(2, 0), Qt::CheckStateRole).toBool());
0254     delete action;
0255 
0256     action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("ocg[2].state = true;"));
0257     m_document->processAction(action);
0258     QVERIFY(model->data(model->index(2, 0), Qt::CheckStateRole).toBool());
0259     delete action;
0260 
0261     action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("ocg[3].state = false;"));
0262     m_document->processAction(action);
0263     QVERIFY(!model->data(model->index(3, 0), Qt::CheckStateRole).toBool());
0264     delete action;
0265 
0266     action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("ocg[3].state = true;"));
0267     m_document->processAction(action);
0268     QVERIFY(model->data(model->index(3, 0), Qt::CheckStateRole).toBool());
0269     delete action;
0270 }
0271 
0272 void JSFunctionsTest::testAlert()
0273 {
0274     Okular::ScriptAction *action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("ret = app.alert( \"Random Message\" );"));
0275     QScopedPointer<MessageBoxHelper> messageBoxHelper;
0276     messageBoxHelper.reset(new MessageBoxHelper(QMessageBox::Ok, QStringLiteral("Random Message"), QMessageBox::Critical, QStringLiteral("Okular"), false));
0277     m_document->processAction(action);
0278     delete action;
0279 
0280     action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("ret = app.alert( \"Empty Message\", 1 );"));
0281     messageBoxHelper.reset(new MessageBoxHelper(QMessageBox::Ok, QStringLiteral("Empty Message"), QMessageBox::Warning, QStringLiteral("Okular"), false));
0282     m_document->processAction(action);
0283     delete action;
0284 
0285     action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("ret = app.alert( \"No Message\", 2, 2 );"));
0286     messageBoxHelper.reset(new MessageBoxHelper(QMessageBox::Yes, QStringLiteral("No Message"), QMessageBox::Question, QStringLiteral("Okular"), false));
0287     m_document->processAction(action);
0288     delete action;
0289 
0290     action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("ret = app.alert( \"No\", 3, 2, \"Test Dialog\" );"));
0291     messageBoxHelper.reset(new MessageBoxHelper(QMessageBox::No, QStringLiteral("No"), QMessageBox::Information, QStringLiteral("Test Dialog"), false));
0292     m_document->processAction(action);
0293     delete action;
0294 
0295     action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("var oCheckBox = new Object();\
0296                                                                             ret = app.alert( \"Cancel\", 3, 3, \"Test Dialog\", 0, oCheckBox );"));
0297     messageBoxHelper.reset(new MessageBoxHelper(QMessageBox::Cancel, QStringLiteral("Cancel"), QMessageBox::Information, QStringLiteral("Test Dialog"), true));
0298     m_document->processAction(action);
0299     delete action;
0300 }
0301 
0302 class PopupMenuHelper : public QObject
0303 {
0304     Q_OBJECT
0305 
0306 public:
0307     PopupMenuHelper()
0308         : m_menuFound(false)
0309     {
0310         QTimer::singleShot(0, this, &PopupMenuHelper::closeMenu);
0311     }
0312 
0313     ~PopupMenuHelper() override
0314     {
0315     }
0316 
0317     bool menuFound()
0318     {
0319         return m_menuFound;
0320     }
0321 
0322     const QJsonArray &menuTree()
0323     {
0324         return m_menuTree;
0325     }
0326 
0327 private Q_SLOTS:
0328     void closeMenu()
0329     {
0330         const QWidgetList allToplevelWidgets = QApplication::topLevelWidgets();
0331         QMenu *menu = nullptr;
0332         for (QWidget *w : allToplevelWidgets) {
0333             if (w->objectName() == QStringLiteral("popUpMenuEx") && w->inherits("QMenu")) {
0334                 menu = qobject_cast<QMenu *>(w);
0335 
0336                 // Generate an tree of string with all the menu trees
0337                 processQMenuToJS(menu, m_menuTree);
0338 
0339                 menu->close();
0340                 m_menuFound = true;
0341                 break;
0342             }
0343         }
0344         if (!menu) {
0345             QTimer::singleShot(0, this, &PopupMenuHelper::closeMenu);
0346         }
0347     }
0348 
0349 private:
0350     static void processQMenuToJS(QMenu *menu, QJsonArray &array)
0351     {
0352         QList<QAction *> actions = menu->actions();
0353 
0354         for (QAction *action : actions) {
0355             QMenu *itMenu = action->menu();
0356 
0357             if (itMenu != nullptr) {
0358                 QJsonArray subMenus;
0359                 subMenus.append(action->text());
0360                 processQMenuToJS(itMenu, subMenus);
0361                 array.append(subMenus);
0362             } else {
0363                 array.append(action->text());
0364             }
0365         }
0366     }
0367 
0368 private:
0369     bool m_menuFound;
0370     QJsonArray m_menuTree;
0371 };
0372 
0373 void JSFunctionsTest::testPopUpMenu()
0374 {
0375     Okular::ScriptAction *action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("ret = app.popUpMenu( [\"Fruits\",\"Apples\",\"Oranges\"], \"-\",\"Beans\",\"Corn\" );"));
0376 
0377     QScopedPointer<PopupMenuHelper> popupMenuHelper;
0378     popupMenuHelper.reset(new PopupMenuHelper());
0379     m_document->processAction(action);
0380     QJsonArray expected = {
0381         QJsonArray {
0382             QJsonValue {QStringLiteral("Fruits")},
0383             QJsonValue {QStringLiteral("Apples")},
0384             QJsonValue {QStringLiteral("Oranges")},
0385         },
0386         QJsonValue {QStringLiteral("-")},
0387         QJsonValue {QStringLiteral("Beans")},
0388         QJsonValue {QStringLiteral("Corn")},
0389     };
0390 
0391     QString expectedString = QString::fromUtf8(QJsonDocument(expected).toJson());
0392     QString resultString = QString::fromUtf8(QJsonDocument(popupMenuHelper->menuTree()).toJson());
0393     QString description = QStringLiteral("Expected:\n") + expectedString + QStringLiteral("But got:\n") + resultString;
0394     QVERIFY2(expected == popupMenuHelper->menuTree(), description.toUtf8().constData());
0395 
0396     delete action;
0397 }
0398 
0399 void JSFunctionsTest::testPopUpMenuEx()
0400 {
0401     QScopedPointer<PopupMenuHelper> popupMenuHelper;
0402 
0403     Okular::ScriptAction *action = new Okular::ScriptAction(
0404         Okular::JavaScript,
0405         QStringLiteral("ret = app.popUpMenuEx( {cName:\"Fruits\", oSubMenu:[{cName:\"Apples\", bMarked:false},{cName:\"Oranges\", bMarked:true}]}, {cName:\"-\"},{cName:\"Beans\", bEnabled:false},{cName:\"Corn\", bEnabled:true} )"));
0406 
0407     popupMenuHelper.reset(new PopupMenuHelper());
0408     m_document->processAction(action);
0409     QVERIFY(popupMenuHelper->menuFound());
0410 
0411     QJsonArray expected = {
0412         QJsonArray {
0413             QJsonValue {QStringLiteral("Fruits")},
0414             QJsonValue {QStringLiteral("Apples")},
0415             QJsonValue {QStringLiteral("Oranges")},
0416         },
0417         QJsonValue {QStringLiteral("-")},
0418         QJsonValue {QStringLiteral("Beans")},
0419         QJsonValue {QStringLiteral("Corn")},
0420     };
0421 
0422     QString expectedString = QString::fromUtf8(QJsonDocument(expected).toJson());
0423     QString resultString = QString::fromUtf8(QJsonDocument(popupMenuHelper->menuTree()).toJson());
0424     QString description = QStringLiteral("Expected:\n") + expectedString + QStringLiteral("But got:\n") + resultString;
0425     QVERIFY2(expected == popupMenuHelper->menuTree(), description.toUtf8().constData());
0426 
0427     delete action;
0428 
0429     // Test infinite recursion
0430     action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("\
0431         var recursiveMenu = {\"cName\": \"Devil menu\"};\n\
0432         recursiveMenu.oSubMenu = [ recursiveMenu ];\n\
0433         ret = app.popUpMenuEx( recursiveMenu );"));
0434 
0435     popupMenuHelper.reset(new PopupMenuHelper());
0436     m_document->processAction(action);
0437     QVERIFY(popupMenuHelper->menuFound());
0438     // Must not crash
0439     delete action;
0440 }
0441 
0442 /** @brief Checks a single JS action against an expected result
0443  *
0444  * Runs an action with the given @p script and checks that it
0445  * does pop-up a messagebox with the given @p result text.
0446  */
0447 class PrintDHelper
0448 {
0449 public:
0450     PrintDHelper(Okular::Document *document, const QString &script, const QString &result)
0451         : action(new Okular::ScriptAction(Okular::JavaScript, script))
0452         , box(new MessageBoxHelper(QMessageBox::Ok, result, QMessageBox::Critical, QStringLiteral("Okular"), false))
0453     {
0454         document->processAction(action.data());
0455     }
0456 
0457 private:
0458     QScopedPointer<Okular::ScriptAction> action;
0459     QScopedPointer<MessageBoxHelper> box;
0460 };
0461 
0462 void JSFunctionsTest::testPrintD_data()
0463 {
0464     // Force consistent locale
0465     QLocale locale(QStringLiteral("en_US"));
0466     QLocale::setDefault(locale);
0467 
0468     QTest::addColumn<QString>("script");
0469     QTest::addColumn<QString>("result");
0470 
0471     QTest::newRow("mmyyy") << QStringLiteral(
0472                                   "var date = new Date( 2010, 0, 5, 11, 10, 32, 1 );\
0473                             ret = app.alert( util.printd( \"mm\\\\yyyy\", date ) );")
0474                            << QStringLiteral("01\\2010");
0475     QTest::newRow("myy") << QStringLiteral("ret = app.alert( util.printd( \"m\\\\yy\", date ) );") << QStringLiteral("1\\10");
0476     QTest::newRow("ddmmHHMM") << QStringLiteral("ret = app.alert( util.printd( \"dd\\\\mm HH:MM\", date ) );") << QStringLiteral("05\\01 11:10");
0477     QTest::newRow("ddmmHHMMss") << QStringLiteral("ret = app.alert( util.printd( \"dd\\\\mm HH:MM:ss\", date ) );") << QStringLiteral("05\\01 11:10:32");
0478     QTest::newRow("yyyymmHHMMss") << QStringLiteral("ret = app.alert( util.printd( \"yyyy\\\\mm HH:MM:ss\", date ) );") << QStringLiteral("2010\\01 11:10:32");
0479     QTest::newRow("0") << QStringLiteral("ret = app.alert( util.printd( 0, date ) );") << QStringLiteral("D:20100105111032");
0480     QTest::newRow("1") << QStringLiteral("ret = app.alert( util.printd( 1, date ) );") << QStringLiteral("2010.01.05 11:10:32");
0481 
0482     QDate date(2010, 1, 5);
0483     QTest::newRow("2") << QStringLiteral("ret = app.alert( util.printd( 2, date ) );") << QString(date.toString(locale.dateFormat(QLocale::ShortFormat)) + QStringLiteral(" 11:10:32\u202FAM"));
0484 }
0485 
0486 void JSFunctionsTest::testPrintD()
0487 {
0488     QFETCH(QString, script);
0489     QFETCH(QString, result);
0490 
0491     QVERIFY(script.contains(QLatin1String("printd")));
0492     PrintDHelper test(m_document, script, result);
0493 }
0494 
0495 void JSFunctionsTest::cleanupTestCase()
0496 {
0497     m_document->closeDocument();
0498     delete m_document;
0499 }
0500 
0501 QTEST_MAIN(JSFunctionsTest)
0502 #include "jsfunctionstest.moc"