File indexing completed on 2024-05-12 05:15:00

0001 /*
0002   This file is part of the kcalcore library.
0003 
0004   SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.net>
0005 
0006   SPDX-License-Identifier: LGPL-2.0-or-later
0007 */
0008 
0009 #include "testincidenceformatter.h"
0010 #include "test_config.h"
0011 
0012 #include "grantleetemplatemanager_p.h"
0013 #include "incidenceformatter.h"
0014 
0015 #include <KCalendarCore/Event>
0016 #include <KCalendarCore/FreeBusy>
0017 #include <KCalendarCore/ICalFormat>
0018 #include <KCalendarCore/Journal>
0019 #include <KCalendarCore/MemoryCalendar>
0020 #include <KCalendarCore/Todo>
0021 
0022 #include <KLocalizedString>
0023 
0024 #include <QDebug>
0025 #include <QIcon>
0026 #include <QLocale>
0027 #include <QProcess>
0028 #include <QRegularExpression>
0029 #include <QStandardPaths>
0030 #include <QTest>
0031 #include <QTimeZone>
0032 QTEST_MAIN(IncidenceFormatterTest)
0033 #ifndef Q_OS_WIN
0034 void initLocale()
0035 {
0036     setenv("LC_ALL", "en_US.utf-8", 1);
0037     setenv("TZ", "UTC", 1);
0038 }
0039 
0040 Q_CONSTRUCTOR_FUNCTION(initLocale)
0041 #endif
0042 using namespace KCalendarCore;
0043 using namespace KCalUtils;
0044 
0045 // Button colors.
0046 static QString btnBg;
0047 static QString btnFg;
0048 static QString btnBdr;
0049 static QString btnHl;
0050 
0051 void IncidenceFormatterTest::initTestCase()
0052 {
0053     QStandardPaths::setTestModeEnabled(true);
0054     GrantleeTemplateManager::instance()->setTemplatePath(QStringLiteral(TEST_TEMPLATE_PATH));
0055     GrantleeTemplateManager::instance()->setPluginPath(QStringLiteral(TEST_PLUGIN_PATH));
0056     QIcon::setThemeName(QStringLiteral("oxygen"));
0057     QLocale::setDefault(QLocale(QStringLiteral("C")));
0058 
0059     QPalette palette;
0060     palette.setCurrentColorGroup(QPalette::Normal);
0061     btnBg = palette.color(QPalette::Button).name();
0062     btnBdr = palette.shadow().color().name();
0063     btnFg = palette.color(QPalette::ButtonText).name();
0064     palette.setCurrentColorGroup(QPalette::Active);
0065     btnHl = palette.shadow().color().name();
0066 }
0067 
0068 void IncidenceFormatterTest::testRecurrenceString()
0069 {
0070     // TEST: A daily recurrence with date exclusions //
0071     Event::Ptr e1 = Event::Ptr(new Event());
0072 
0073     QDate day(2010, 10, 3);
0074     QTime tim(12, 0, 0);
0075     QDateTime kdt(day, tim, QTimeZone::utc());
0076     e1->setDtStart(kdt);
0077     e1->setDtEnd(kdt.addSecs(60 * 60)); // 1hr event
0078 
0079     QCOMPARE(IncidenceFormatter::recurrenceString(e1), i18n("No recurrence"));
0080 
0081     Recurrence *r1 = e1->recurrence();
0082 
0083     r1->setDaily(1);
0084     r1->setEndDateTime(kdt.addDays(5)); // ends 5 days from now
0085     QString endDateStr = QLocale().toString(kdt.addDays(5).toLocalTime(), QLocale::ShortFormat);
0086     QCOMPARE(IncidenceFormatter::recurrenceString(e1), i18n("Recurs daily until %1", endDateStr));
0087 
0088     r1->setFrequency(2);
0089 
0090     QCOMPARE(IncidenceFormatter::recurrenceString(e1), i18n("Recurs every 2 days until %1", endDateStr));
0091 
0092     r1->addExDate(kdt.addDays(1).date());
0093     QString exDateStr = QLocale().toString(kdt.addDays(1).date(), QLocale::ShortFormat);
0094     QCOMPARE(IncidenceFormatter::recurrenceString(e1), i18n("Recurs every 2 days until %1 (excluding %2)", endDateStr, exDateStr));
0095 
0096     r1->addExDate(kdt.addDays(3).date());
0097     QString exDateStr2 = QLocale().toString(kdt.addDays(3).date(), QLocale::ShortFormat);
0098     QCOMPARE(IncidenceFormatter::recurrenceString(e1), i18n("Recurs every 2 days until %1 (excluding %2,%3)", endDateStr, exDateStr, exDateStr2));
0099 
0100     // TEST: An daily recurrence, with datetime exclusions //
0101     Event::Ptr e2 = Event::Ptr(new Event());
0102     e2->setDtStart(kdt);
0103     e2->setDtEnd(kdt.addSecs(60 * 60)); // 1hr event
0104 
0105     Recurrence *r2 = e2->recurrence();
0106 
0107     r2->setDaily(1);
0108     r2->setEndDate(kdt.addDays(5).date()); // ends 5 days from now
0109     QCOMPARE(IncidenceFormatter::recurrenceString(e2), i18n("Recurs daily until %1", endDateStr));
0110 
0111     r2->setFrequency(2);
0112 
0113     QCOMPARE(IncidenceFormatter::recurrenceString(e2), i18n("Recurs every 2 days until %1", endDateStr));
0114 
0115     r2->addExDateTime(kdt.addDays(1));
0116     QCOMPARE(IncidenceFormatter::recurrenceString(e2), i18n("Recurs every 2 days until %1 (excluding %2)", endDateStr, exDateStr));
0117 
0118     r2->addExDate(kdt.addDays(3).date());
0119     QCOMPARE(IncidenceFormatter::recurrenceString(e2), i18n("Recurs every 2 days until %1 (excluding %2,%3)", endDateStr, exDateStr, exDateStr2));
0120 
0121     // TEST: An hourly recurrence, with exclusions //
0122     Event::Ptr e3 = Event::Ptr(new Event());
0123     e3->setDtStart(kdt);
0124     e3->setDtEnd(kdt.addSecs(60 * 60)); // 1hr event
0125 
0126     Recurrence *r3 = e3->recurrence();
0127 
0128     r3->setHourly(1);
0129     r3->setEndDateTime(kdt.addSecs(5 * 60 * 60)); // ends 5 hrs from now
0130     endDateStr = QLocale().toString(r3->endDateTime().toLocalTime(), QLocale::ShortFormat);
0131     QCOMPARE(IncidenceFormatter::recurrenceString(e3), i18n("Recurs hourly until %1", endDateStr));
0132 
0133     r3->setFrequency(2);
0134 
0135     QCOMPARE(IncidenceFormatter::recurrenceString(e3), i18n("Recurs every 2 hours until %1", endDateStr));
0136 
0137     r3->addExDateTime(kdt.addSecs(1 * 60 * 60));
0138     QString hourStr = QLocale().toString(QTime(13, 0), QLocale::ShortFormat);
0139     QCOMPARE(IncidenceFormatter::recurrenceString(e3), i18n("Recurs every 2 hours until %1 (excluding %2)", endDateStr, hourStr));
0140 
0141     r3->addExDateTime(kdt.addSecs(3 * 60 * 60));
0142     QString hourStr2 = QLocale().toString(QTime(15, 0), QLocale::ShortFormat);
0143     QCOMPARE(IncidenceFormatter::recurrenceString(e3), i18n("Recurs every 2 hours until %1 (excluding %2,%3)", endDateStr, hourStr, hourStr2));
0144 
0145     //  qDebug() << "recurrenceString=" << IncidenceFormatter::recurrenceString( e3 );
0146 }
0147 
0148 KCalendarCore::Calendar::Ptr IncidenceFormatterTest::loadCalendar(const QString &name)
0149 {
0150     auto calendar = KCalendarCore::MemoryCalendar::Ptr::create(QTimeZone::utc());
0151     KCalendarCore::ICalFormat format;
0152 
0153     if (!format.load(calendar, QStringLiteral(TEST_DATA_DIR "/%1.ical").arg(name))) {
0154         return KCalendarCore::Calendar::Ptr();
0155     }
0156 
0157     return calendar;
0158 }
0159 
0160 bool IncidenceFormatterTest::validateHtml(const QString &name, const QString &_html)
0161 {
0162     QString html = QStringLiteral(
0163                        "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">\n"
0164                        "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n"
0165                        "  <head>\n"
0166                        "    <title></title>\n"
0167                        "    <style></style>\n"
0168                        "  </head>\n"
0169                        "<body>")
0170         + _html + QStringLiteral("</body>\n</html>");
0171 
0172     const QString outFileName = QStringLiteral(TEST_DATA_DIR "/%1.out").arg(name);
0173     const QString htmlFileName = QStringLiteral(TEST_DATA_DIR "/%1.out.html").arg(name);
0174     QFile outFile(outFileName);
0175     if (!outFile.open(QIODevice::WriteOnly)) {
0176         return false;
0177     }
0178     outFile.write(html.toUtf8());
0179     outFile.close();
0180 
0181     // validate xml and pretty-print for comparison
0182     // TODO add proper cmake check for xmllint and diff
0183     const QStringList args =
0184         {QStringLiteral("--format"), QStringLiteral("--encode"), QStringLiteral("UTF8"), QStringLiteral("--output"), htmlFileName, outFileName};
0185 
0186     const int result = QProcess::execute(QStringLiteral("xmllint"), args);
0187     return result == 0;
0188 }
0189 
0190 bool IncidenceFormatterTest::compareHtml(const QString &name)
0191 {
0192     const QString htmlFileName = QStringLiteral(TEST_DATA_DIR "/%1.out.html").arg(name);
0193     const QString referenceFileName = QStringLiteral(TEST_DATA_DIR "/%1.html").arg(name);
0194 
0195     // get rid of system dependent or random paths
0196     {
0197         QFile f(htmlFileName);
0198         if (!f.open(QIODevice::ReadOnly)) {
0199             return false;
0200         }
0201         QString content = QString::fromUtf8(f.readAll());
0202         f.close();
0203         content.replace(QRegularExpression(QStringLiteral("\"file:[^\"]*[/(?:%2F)]([^\"/(?:%2F)]*)\"")), QStringLiteral("\"file:\\1\""));
0204         // emoticons give us absolute paths without file:
0205         content.replace(QRegularExpression(QStringLiteral("src=\"/[^\"]*[/(?:%2F)]([^\"/(?:%2F)]*)\"")), QStringLiteral("src=\"\\1\""));
0206         // icon filename extensions depend on used theme, Oxygen has PNG, Breeze has SVG
0207         content.replace(QRegularExpression(QStringLiteral(".(png|svg)\"")), QStringLiteral("\""));
0208         if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
0209             return false;
0210         }
0211         f.write(content.toUtf8());
0212         f.close();
0213     }
0214 
0215     // compare to reference file
0216     const QStringList args = {QStringLiteral("-u"), referenceFileName, htmlFileName};
0217 
0218     QProcess proc;
0219     proc.setProcessChannelMode(QProcess::ForwardedChannels);
0220     proc.start(QStringLiteral("diff"), args);
0221     if (!proc.waitForFinished()) {
0222         return false;
0223     }
0224 
0225     return proc.exitCode() == 0;
0226 }
0227 
0228 void IncidenceFormatterTest::cleanup(const QString &name)
0229 {
0230     QFile::remove(QStringLiteral(TEST_DATA_DIR "/%1.out").arg(name));
0231     QFile::remove(QStringLiteral(TEST_DATA_DIR "/%1.out.html").arg(name));
0232 }
0233 
0234 void IncidenceFormatterTest::testErrorTemplate()
0235 {
0236     GrantleeTemplateManager::instance()->setTemplatePath(QStringLiteral(TEST_DATA_DIR));
0237     const QString html = GrantleeTemplateManager::instance()->render(QStringLiteral("broken-template.html"), QVariantHash());
0238     GrantleeTemplateManager::instance()->setTemplatePath(QStringLiteral(TEST_TEMPLATE_PATH));
0239 
0240     const QString expected = QStringLiteral(
0241         "<h1>Template parsing error</h1>\n"
0242         "<b>Template:</b> broken-template.html<br>\n"
0243         "<b>Error message:</b> Unclosed tag in template broken-template.html. Expected one of: (elif else endif), line 2, broken-template.html");
0244 
0245     QCOMPARE(html, expected);
0246 }
0247 
0248 void IncidenceFormatterTest::testDisplayViewFormatEvent_data()
0249 {
0250     QTest::addColumn<QString>("name");
0251 
0252     QTest::newRow("event-1") << QStringLiteral("event-1");
0253     QTest::newRow("event-2") << QStringLiteral("event-2");
0254     QTest::newRow("event-exception-thisandfuture") << QStringLiteral("event-exception-thisandfuture");
0255     QTest::newRow("event-exception-single") << QStringLiteral("event-exception-single");
0256     QTest::newRow("event-allday-multiday") << QStringLiteral("event-allday-multiday");
0257     QTest::newRow("event-allday") << QStringLiteral("event-allday");
0258     QTest::newRow("event-multiday") << QStringLiteral("event-multiday");
0259 }
0260 
0261 void IncidenceFormatterTest::testDisplayViewFormatEvent()
0262 {
0263     QFETCH(QString, name);
0264 
0265     KCalendarCore::Calendar::Ptr calendar = loadCalendar(name);
0266     QVERIFY(calendar);
0267 
0268     const auto events = calendar->events();
0269     QCOMPARE(events.size(), 1);
0270 
0271     const QString html = IncidenceFormatter::extensiveDisplayStr(calendar, events[0]);
0272 
0273     QVERIFY(validateHtml(name, html));
0274     QVERIFY(compareHtml(name));
0275 
0276     cleanup(name);
0277 }
0278 
0279 void IncidenceFormatterTest::testDisplayViewFormatTodo_data()
0280 {
0281     QTest::addColumn<QString>("name");
0282 
0283     QTest::newRow("todo-1") << QStringLiteral("todo-1");
0284     QTest::newRow("todo-2") << QStringLiteral("todo-2");
0285 }
0286 
0287 void IncidenceFormatterTest::testDisplayViewFormatTodo()
0288 {
0289     QFETCH(QString, name);
0290 
0291     KCalendarCore::Calendar::Ptr calendar = loadCalendar(name);
0292     QVERIFY(calendar);
0293 
0294     const auto todos = calendar->todos();
0295     QCOMPARE(todos.size(), 1);
0296 
0297     const QString html = IncidenceFormatter::extensiveDisplayStr(calendar, todos[0]);
0298 
0299     QVERIFY(validateHtml(name, html));
0300     QVERIFY(compareHtml(name));
0301 
0302     cleanup(name);
0303 }
0304 
0305 void IncidenceFormatterTest::testDisplayViewFormatJournal_data()
0306 {
0307     QTest::addColumn<QString>("name");
0308 
0309     QTest::newRow("journal-1") << QStringLiteral("journal-1");
0310 }
0311 
0312 void IncidenceFormatterTest::testDisplayViewFormatJournal()
0313 {
0314     QFETCH(QString, name);
0315 
0316     KCalendarCore::Calendar::Ptr calendar = loadCalendar(name);
0317     QVERIFY(calendar);
0318 
0319     const auto journals = calendar->journals();
0320     QCOMPARE(journals.size(), 1);
0321 
0322     const QString html = IncidenceFormatter::extensiveDisplayStr(calendar, journals[0]);
0323 
0324     QVERIFY(validateHtml(name, html));
0325     QVERIFY(compareHtml(name));
0326 
0327     cleanup(name);
0328 }
0329 
0330 void IncidenceFormatterTest::testDisplayViewFreeBusy_data()
0331 {
0332     QTest::addColumn<QString>("name");
0333 
0334     QTest::newRow("freebusy-1") << QStringLiteral("freebusy-1");
0335 }
0336 
0337 void IncidenceFormatterTest::testDisplayViewFreeBusy()
0338 {
0339     QFETCH(QString, name);
0340 
0341     KCalendarCore::Calendar::Ptr calendar = loadCalendar(name);
0342     QVERIFY(calendar);
0343 
0344     QFile file(QStringLiteral(TEST_DATA_DIR "/%1.ical").arg(name));
0345     QVERIFY(file.open(QIODevice::ReadOnly));
0346     const QByteArray fbData = file.readAll();
0347 
0348     KCalendarCore::ICalFormat format;
0349     KCalendarCore::FreeBusy::Ptr freeBusy = format.parseFreeBusy(QString::fromUtf8(fbData));
0350     QVERIFY(freeBusy);
0351 
0352     const QString html = IncidenceFormatter::extensiveDisplayStr(calendar, freeBusy);
0353 
0354     QVERIFY(validateHtml(name, html));
0355     QVERIFY(compareHtml(name));
0356 
0357     cleanup(name);
0358 }
0359 
0360 void IncidenceFormatterTest::testFormatIcalInvitation_data()
0361 {
0362     QTest::addColumn<QString>("name");
0363 
0364     QTest::newRow("itip-journal") << QStringLiteral("itip-journal");
0365     QTest::newRow("itip-journal-delegation-request") << QStringLiteral("itip-journal-delegation-request");
0366     QTest::newRow("itip-journal-delegation-reply") << QStringLiteral("itip-journal-delegation-reply");
0367     QTest::newRow("itip-journal-declined-reply") << QStringLiteral("itip-journal-declined-reply");
0368     QTest::newRow("itip-journal-tentative-reply") << QStringLiteral("itip-journal-tentative-reply");
0369     QTest::newRow("itip-journal-accepted-reply") << QStringLiteral("itip-journal-accepted-reply");
0370 
0371     QTest::newRow("itip-todo") << QStringLiteral("itip-todo");
0372     QTest::newRow("itip-todo-with-start") << QStringLiteral("itip-todo-with-start");
0373     QTest::newRow("itip-todo-delegation-request") << QStringLiteral("itip-todo-delegation-request");
0374     QTest::newRow("itip-todo-delegation-reply") << QStringLiteral("itip-todo-delegation-reply");
0375     QTest::newRow("itip-todo-declined-reply") << QStringLiteral("itip-todo-declined-reply");
0376     QTest::newRow("itip-todo-tentative-reply") << QStringLiteral("itip-todo-tentative-reply");
0377     QTest::newRow("itip-todo-accepted-reply") << QStringLiteral("itip-todo-accepted-reply");
0378 
0379     QTest::newRow("itip-event-with-html-description") << QStringLiteral("itip-event-with-html-description");
0380     QTest::newRow("itip-event-with-recurrence-attachment-reminder") << QStringLiteral("itip-event-with-recurrence-attachment-reminder");
0381     QTest::newRow("itip-event-multiday-allday") << QStringLiteral("itip-event-multiday-allday");
0382     QTest::newRow("itip-event-multiday") << QStringLiteral("itip-event-multiday");
0383     QTest::newRow("itip-event-allday") << QStringLiteral("itip-event-allday");
0384     QTest::newRow("itip-event") << QStringLiteral("itip-event");
0385     QTest::newRow("itip-event-request") << QStringLiteral("itip-event-request");
0386     QTest::newRow("itip-event-counterproposal") << QStringLiteral("itip-event-counterproposal");
0387     QTest::newRow("itip-event-counterproposal-declined") << QStringLiteral("itip-event-counterproposal-declined");
0388     QTest::newRow("itip-event-delegation-request") << QStringLiteral("itip-event-delegation-request");
0389     QTest::newRow("itip-event-delegation-reply") << QStringLiteral("itip-event-delegation-reply");
0390     QTest::newRow("itip-event-declined-reply") << QStringLiteral("itip-event-delegation-reply");
0391     QTest::newRow("itip-event-tentative-reply") << QStringLiteral("itip-event-tentative-reply");
0392     QTest::newRow("itip-event-accepted-reply") << QStringLiteral("itip-event-accepted-reply");
0393 }
0394 
0395 void IncidenceFormatterTest::testFormatIcalInvitation()
0396 {
0397     QFETCH(QString, name);
0398 
0399     KCalendarCore::MemoryCalendar::Ptr calendar(new KCalendarCore::MemoryCalendar(QTimeZone::utc()));
0400     InvitationFormatterHelper helper;
0401 
0402     QFile eventFile(QStringLiteral(TEST_DATA_DIR "/%1.ical").arg(name));
0403     QVERIFY(eventFile.exists());
0404     QVERIFY(eventFile.open(QIODevice::ReadOnly));
0405     const QByteArray data = eventFile.readAll();
0406 
0407     const QString html = IncidenceFormatter::formatICalInvitation(QString::fromUtf8(data), calendar, &helper)
0408                              .replace(btnBg, QStringLiteral("btnBg"))
0409                              .replace(btnFg, QStringLiteral("btnFg"))
0410                              .replace(btnBdr, QStringLiteral("btnBdr"));
0411 
0412     QVERIFY(validateHtml(name, html));
0413     QVERIFY(compareHtml(name));
0414 
0415     cleanup(name);
0416 }
0417 
0418 #include "moc_testincidenceformatter.cpp"