/* SPDX-FileCopyrightText: 2019 João Netto SPDX-License-Identifier: GPL-2.0-or-later */ #include #include #include #include #include #include #include "../settings_core.h" #include "core/action.h" #include "core/document.h" #include "core/scripter.h" #include #include #include #include #include #include #include #include #include #include class MessageBoxHelper : public QObject { Q_OBJECT public: MessageBoxHelper(QMessageBox::StandardButton b, QString message, QMessageBox::Icon icon, QString title, bool hasCheckBox) : m_button(b) , m_clicked(false) , m_message(std::move(message)) , m_icon(icon) , m_title(std::move(title)) , m_checkBox(hasCheckBox) { QTimer::singleShot(0, this, &MessageBoxHelper::closeMessageBox); } ~MessageBoxHelper() override { QVERIFY(m_clicked); } private Q_SLOTS: void closeMessageBox() { const QWidgetList allToplevelWidgets = QApplication::topLevelWidgets(); QMessageBox *mb = nullptr; for (QWidget *w : allToplevelWidgets) { if (w->inherits("QMessageBox")) { mb = qobject_cast(w); QCOMPARE(mb->text(), m_message); QCOMPARE(mb->windowTitle(), m_title); QCOMPARE(mb->icon(), m_icon); QCheckBox *box = mb->checkBox(); QCOMPARE(box != nullptr, m_checkBox); mb->button(m_button)->click(); } } if (!mb) { QTimer::singleShot(0, this, &MessageBoxHelper::closeMessageBox); return; } m_clicked = true; } private: QMessageBox::StandardButton m_button; bool m_clicked; QString m_message; QMessageBox::Icon m_icon; QString m_title; bool m_checkBox; }; class JSFunctionsTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void testNthFieldName(); void testDisplay(); void testSetClearInterval(); void testSetClearTimeOut(); void testGetOCGs(); void cleanupTestCase(); void testAlert(); void testPopUpMenu(); void testPopUpMenuEx(); void testPrintD(); void testPrintD_data(); private: Okular::Document *m_document; QMap m_fields; }; void JSFunctionsTest::initTestCase() { Okular::SettingsCore::instance(QStringLiteral("jsfunctionstest")); m_document = new Okular::Document(nullptr); const QString testFile = QStringLiteral(KDESRCDIR "data/kjsfunctionstest.pdf"); QMimeDatabase db; const QMimeType mime = db.mimeTypeForFile(testFile); QCOMPARE(m_document->openDocument(testFile, QUrl(), mime), Okular::Document::OpenSuccess); const Okular::Page *page = m_document->page(0); const QList pageFormFields = page->formFields(); for (Okular::FormField *ff : pageFormFields) { m_fields.insert(ff->name(), ff); } } void JSFunctionsTest::testNthFieldName() { for (int i = 0; i < 21; ++i) { Okular::ScriptAction *action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("var field = Doc.getField( Doc.getNthFieldName(%1) );\ field.display = display.visible;") .arg(i)); m_document->processAction(action); QVERIFY(m_fields[QStringLiteral("0.%1").arg(i)]->isVisible()); m_fields[QStringLiteral("0.%1").arg(i)]->setVisible(false); delete action; } } void JSFunctionsTest::testDisplay() { Okular::ScriptAction *action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("field = Doc.getField(\"0.0\");field.display=display.hidden;\ field = Doc.getField(\"0.10\");field.display=display.visible;")); m_document->processAction(action); QVERIFY(!m_fields[QStringLiteral("0.0")]->isVisible()); QVERIFY(!m_fields[QStringLiteral("0.0")]->isPrintable()); QVERIFY(m_fields[QStringLiteral("0.10")]->isVisible()); QVERIFY(m_fields[QStringLiteral("0.10")]->isPrintable()); delete action; action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("field = Doc.getField(\"0.10\");field.display=display.noView;\ field = Doc.getField(\"0.15\");field.display=display.noPrint;")); m_document->processAction(action); QVERIFY(!m_fields[QStringLiteral("0.10")]->isVisible()); QVERIFY(m_fields[QStringLiteral("0.10")]->isPrintable()); QVERIFY(m_fields[QStringLiteral("0.15")]->isVisible()); QVERIFY(!m_fields[QStringLiteral("0.15")]->isPrintable()); delete action; action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("field = Doc.getField(\"0.15\");field.display=display.hidden;\ field = Doc.getField(\"0.20\");field.display=display.visible;")); m_document->processAction(action); QVERIFY(!m_fields[QStringLiteral("0.15")]->isVisible()); QVERIFY(!m_fields[QStringLiteral("0.15")]->isPrintable()); QVERIFY(m_fields[QStringLiteral("0.20")]->isVisible()); QVERIFY(m_fields[QStringLiteral("0.20")]->isPrintable()); delete action; action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("field = Doc.getField(\"0.20\");field.display=display.hidden;\ field = Doc.getField(\"0.0\");field.display=display.visible;")); m_document->processAction(action); QVERIFY(!m_fields[QStringLiteral("0.20")]->isVisible()); QVERIFY(!m_fields[QStringLiteral("0.20")]->isPrintable()); QVERIFY(m_fields[QStringLiteral("0.0")]->isVisible()); QVERIFY(m_fields[QStringLiteral("0.0")]->isPrintable()); delete action; } void delay() { QTime dieTime = QTime::currentTime().addSecs(2); while (QTime::currentTime() < dieTime) { QCoreApplication::processEvents(QEventLoop::AllEvents, 100); } } void JSFunctionsTest::testSetClearInterval() { Okular::ScriptAction *action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("obj = new Object();obj.idx=0;\ obj.inc=function(){field = Doc.getField(Doc.getNthFieldName(obj.idx));\ field.display = display.visible;\ obj.idx = obj.idx + 1;};\ intv = app.setInterval('obj.inc()', 450);obj.idx;")); m_document->processAction(action); QVERIFY(m_fields[QStringLiteral("0.0")]->isVisible()); QVERIFY(!m_fields[QStringLiteral("0.3")]->isVisible()); delete action; delay(); action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("app.clearInterval(intv);obj.idx;")); m_document->processAction(action); QVERIFY(m_fields[QStringLiteral("0.3")]->isVisible()); delete action; } void JSFunctionsTest::testSetClearTimeOut() { Okular::ScriptAction *action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("intv = app.setTimeOut('obj.inc()', 1);obj.idx;")); m_document->processAction(action); QVERIFY(m_fields[QStringLiteral("0.3")]->isVisible()); QVERIFY(!m_fields[QStringLiteral("0.4")]->isVisible()); delay(); delete action; QVERIFY(m_fields[QStringLiteral("0.4")]->isVisible()); action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("intv = app.setTimeOut('obj.inc()', 2000);obj.idx;")); m_document->processAction(action); QVERIFY(m_fields[QStringLiteral("0.4")]->isVisible()); delete action; action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("app.clearTimeOut(intv);obj.idx;")); m_document->processAction(action); QVERIFY(m_fields[QStringLiteral("0.4")]->isVisible()); delay(); QVERIFY(m_fields[QStringLiteral("0.4")]->isVisible()); delete action; } void JSFunctionsTest::testGetOCGs() { QAbstractItemModel *model = m_document->layersModel(); Okular::ScriptAction *action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("var ocg = this.getOCGs(this.pageNum);\ ocg[0].state = false;")); m_document->processAction(action); QVERIFY(!model->data(model->index(0, 0), Qt::CheckStateRole).toBool()); delete action; action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("ocg[0].state = true;")); m_document->processAction(action); QVERIFY(model->data(model->index(0, 0), Qt::CheckStateRole).toBool()); delete action; action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("ocg[1].state = false;")); m_document->processAction(action); QVERIFY(!model->data(model->index(1, 0), Qt::CheckStateRole).toBool()); delete action; action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("ocg[1].state = true;")); m_document->processAction(action); QVERIFY(model->data(model->index(1, 0), Qt::CheckStateRole).toBool()); delete action; action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("ocg[2].state = false;")); m_document->processAction(action); QVERIFY(!model->data(model->index(2, 0), Qt::CheckStateRole).toBool()); delete action; action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("ocg[2].state = true;")); m_document->processAction(action); QVERIFY(model->data(model->index(2, 0), Qt::CheckStateRole).toBool()); delete action; action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("ocg[3].state = false;")); m_document->processAction(action); QVERIFY(!model->data(model->index(3, 0), Qt::CheckStateRole).toBool()); delete action; action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("ocg[3].state = true;")); m_document->processAction(action); QVERIFY(model->data(model->index(3, 0), Qt::CheckStateRole).toBool()); delete action; } void JSFunctionsTest::testAlert() { Okular::ScriptAction *action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("ret = app.alert( \"Random Message\" );")); QScopedPointer messageBoxHelper; messageBoxHelper.reset(new MessageBoxHelper(QMessageBox::Ok, QStringLiteral("Random Message"), QMessageBox::Critical, QStringLiteral("Okular"), false)); m_document->processAction(action); delete action; action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("ret = app.alert( \"Empty Message\", 1 );")); messageBoxHelper.reset(new MessageBoxHelper(QMessageBox::Ok, QStringLiteral("Empty Message"), QMessageBox::Warning, QStringLiteral("Okular"), false)); m_document->processAction(action); delete action; action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("ret = app.alert( \"No Message\", 2, 2 );")); messageBoxHelper.reset(new MessageBoxHelper(QMessageBox::Yes, QStringLiteral("No Message"), QMessageBox::Question, QStringLiteral("Okular"), false)); m_document->processAction(action); delete action; action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("ret = app.alert( \"No\", 3, 2, \"Test Dialog\" );")); messageBoxHelper.reset(new MessageBoxHelper(QMessageBox::No, QStringLiteral("No"), QMessageBox::Information, QStringLiteral("Test Dialog"), false)); m_document->processAction(action); delete action; action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("var oCheckBox = new Object();\ ret = app.alert( \"Cancel\", 3, 3, \"Test Dialog\", 0, oCheckBox );")); messageBoxHelper.reset(new MessageBoxHelper(QMessageBox::Cancel, QStringLiteral("Cancel"), QMessageBox::Information, QStringLiteral("Test Dialog"), true)); m_document->processAction(action); delete action; } class PopupMenuHelper : public QObject { Q_OBJECT public: PopupMenuHelper() : m_menuFound(false) { QTimer::singleShot(0, this, &PopupMenuHelper::closeMenu); } ~PopupMenuHelper() override { } bool menuFound() { return m_menuFound; } const QJsonArray &menuTree() { return m_menuTree; } private Q_SLOTS: void closeMenu() { const QWidgetList allToplevelWidgets = QApplication::topLevelWidgets(); QMenu *menu = nullptr; for (QWidget *w : allToplevelWidgets) { if (w->objectName() == QStringLiteral("popUpMenuEx") && w->inherits("QMenu")) { menu = qobject_cast(w); // Generate an tree of string with all the menu trees processQMenuToJS(menu, m_menuTree); menu->close(); m_menuFound = true; break; } } if (!menu) { QTimer::singleShot(0, this, &PopupMenuHelper::closeMenu); } } private: static void processQMenuToJS(QMenu *menu, QJsonArray &array) { const QList actions = menu->actions(); for (QAction *action : actions) { QMenu *itMenu = action->menu(); if (itMenu != nullptr) { QJsonArray subMenus; subMenus.append(action->text()); processQMenuToJS(itMenu, subMenus); array.append(subMenus); } else { array.append(action->text()); } } } private: bool m_menuFound; QJsonArray m_menuTree; }; void JSFunctionsTest::testPopUpMenu() { Okular::ScriptAction *action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("ret = app.popUpMenu( [\"Fruits\",\"Apples\",\"Oranges\"], \"-\",\"Beans\",\"Corn\" );")); QScopedPointer popupMenuHelper; popupMenuHelper.reset(new PopupMenuHelper()); m_document->processAction(action); QJsonArray expected = { QJsonArray { QJsonValue {QStringLiteral("Fruits")}, QJsonValue {QStringLiteral("Apples")}, QJsonValue {QStringLiteral("Oranges")}, }, QJsonValue {QStringLiteral("-")}, QJsonValue {QStringLiteral("Beans")}, QJsonValue {QStringLiteral("Corn")}, }; QString expectedString = QString::fromUtf8(QJsonDocument(expected).toJson()); QString resultString = QString::fromUtf8(QJsonDocument(popupMenuHelper->menuTree()).toJson()); QString description = QStringLiteral("Expected:\n") + expectedString + QStringLiteral("But got:\n") + resultString; QVERIFY2(expected == popupMenuHelper->menuTree(), description.toUtf8().constData()); delete action; } void JSFunctionsTest::testPopUpMenuEx() { QScopedPointer popupMenuHelper; Okular::ScriptAction *action = new Okular::ScriptAction( Okular::JavaScript, QStringLiteral("ret = app.popUpMenuEx( {cName:\"Fruits\", oSubMenu:[{cName:\"Apples\", bMarked:false},{cName:\"Oranges\", bMarked:true}]}, {cName:\"-\"},{cName:\"Beans\", bEnabled:false},{cName:\"Corn\", bEnabled:true} )")); popupMenuHelper.reset(new PopupMenuHelper()); m_document->processAction(action); QVERIFY(popupMenuHelper->menuFound()); QJsonArray expected = { QJsonArray { QJsonValue {QStringLiteral("Fruits")}, QJsonValue {QStringLiteral("Apples")}, QJsonValue {QStringLiteral("Oranges")}, }, QJsonValue {QStringLiteral("-")}, QJsonValue {QStringLiteral("Beans")}, QJsonValue {QStringLiteral("Corn")}, }; QString expectedString = QString::fromUtf8(QJsonDocument(expected).toJson()); QString resultString = QString::fromUtf8(QJsonDocument(popupMenuHelper->menuTree()).toJson()); QString description = QStringLiteral("Expected:\n") + expectedString + QStringLiteral("But got:\n") + resultString; QVERIFY2(expected == popupMenuHelper->menuTree(), description.toUtf8().constData()); delete action; // Test infinite recursion action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("\ var recursiveMenu = {\"cName\": \"Devil menu\"};\n\ recursiveMenu.oSubMenu = [ recursiveMenu ];\n\ ret = app.popUpMenuEx( recursiveMenu );")); popupMenuHelper.reset(new PopupMenuHelper()); m_document->processAction(action); QVERIFY(popupMenuHelper->menuFound()); // Must not crash delete action; } /** @brief Checks a single JS action against an expected result * * Runs an action with the given @p script and checks that it * does pop-up a messagebox with the given @p result text. */ class PrintDHelper { public: PrintDHelper(Okular::Document *document, const QString &script, const QString &result) : action(new Okular::ScriptAction(Okular::JavaScript, script)) , box(new MessageBoxHelper(QMessageBox::Ok, result, QMessageBox::Critical, QStringLiteral("Okular"), false)) { document->processAction(action.data()); } private: QScopedPointer action; QScopedPointer box; }; void JSFunctionsTest::testPrintD_data() { // Force consistent locale QLocale locale(QStringLiteral("en_US")); QLocale::setDefault(locale); QTest::addColumn("script"); QTest::addColumn("result"); QTest::newRow("mmyyy") << QStringLiteral( "var date = new Date( 2010, 0, 5, 11, 10, 32, 1 );\ ret = app.alert( util.printd( \"mm\\\\yyyy\", date ) );") << QStringLiteral("01\\2010"); QTest::newRow("myy") << QStringLiteral("ret = app.alert( util.printd( \"m\\\\yy\", date ) );") << QStringLiteral("1\\10"); QTest::newRow("ddmmHHMM") << QStringLiteral("ret = app.alert( util.printd( \"dd\\\\mm HH:MM\", date ) );") << QStringLiteral("05\\01 11:10"); QTest::newRow("ddmmHHMMss") << QStringLiteral("ret = app.alert( util.printd( \"dd\\\\mm HH:MM:ss\", date ) );") << QStringLiteral("05\\01 11:10:32"); QTest::newRow("yyyymmHHMMss") << QStringLiteral("ret = app.alert( util.printd( \"yyyy\\\\mm HH:MM:ss\", date ) );") << QStringLiteral("2010\\01 11:10:32"); QTest::newRow("0") << QStringLiteral("ret = app.alert( util.printd( 0, date ) );") << QStringLiteral("D:20100105111032"); QTest::newRow("1") << QStringLiteral("ret = app.alert( util.printd( 1, date ) );") << QStringLiteral("2010.01.05 11:10:32"); QDate date(2010, 1, 5); QTest::newRow("2") << QStringLiteral("ret = app.alert( util.printd( 2, date ) );") << QString(date.toString(locale.dateFormat(QLocale::ShortFormat)) + QStringLiteral(" 11:10:32\u202FAM")); } void JSFunctionsTest::testPrintD() { QFETCH(QString, script); QFETCH(QString, result); QVERIFY(script.contains(QLatin1String("printd"))); PrintDHelper test(m_document, script, result); } void JSFunctionsTest::cleanupTestCase() { m_document->closeDocument(); delete m_document; } QTEST_MAIN(JSFunctionsTest) #include "jsfunctionstest.moc"