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"