diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test_config.cpp | 123 | ||||
| -rw-r--r-- | tests/test_keymap.cpp | 20 | ||||
| -rw-r--r-- | tests/test_mainwindow.cpp | 64 | ||||
| -rw-r--r-- | tests/test_messageview.cpp | 55 |
4 files changed, 262 insertions, 0 deletions
diff --git a/tests/test_config.cpp b/tests/test_config.cpp index e492480..24f6afb 100644 --- a/tests/test_config.cpp +++ b/tests/test_config.cpp @@ -34,6 +34,11 @@ private slots: void brokenSyncCommandIsAProblem(); void malformedAccountIsAProblem(); void validConfigHasNoProblems(); + void startupQueryDefaultsToUnread(); + void startupQueryHonoursTheConfiguredName(); + void unknownStartupQueryFallsBackAndReports(); + void generalSectionKeysAreActuallyRead(); + void messageZoomDefaultsAndValidates(); }; static QString writeIni(const QTemporaryDir &dir, const QString &body) @@ -222,5 +227,123 @@ void TestConfig::validConfigHasNoProblems() QVERIFY(config.warnings().isEmpty()); } +void TestConfig::startupQueryDefaultsToUnread() +{ + // [queries] is read through childKeys(), which sorts alphabetically, so + // savedQueries().first() is "Flagged" here. The startup query must be + // chosen by name, not by sort order. + QTemporaryDir dir; + Config config; + config.load(writeIni(dir, QStringLiteral( + "[queries]\n" + "Inbox=tag:inbox\n" + "Unread=tag:unread\n" + "Flagged=tag:flagged\n"))); + + QCOMPARE(config.savedQueries().first().name, QStringLiteral("Flagged")); + QCOMPARE(config.startupSavedQuery().name, QStringLiteral("Unread")); + QCOMPARE(config.startupSavedQuery().query, QStringLiteral("tag:unread")); + QVERIFY(config.problems().isEmpty()); +} + +void TestConfig::startupQueryHonoursTheConfiguredName() +{ + QTemporaryDir dir; + Config config; + config.load(writeIni(dir, QStringLiteral( + "[general]\n" + "startup_query=Flagged\n" + "\n" + "[queries]\n" + "Inbox=tag:inbox\n" + "Unread=tag:unread\n" + "Flagged=tag:flagged\n"))); + + QCOMPARE(config.startupSavedQuery().name, QStringLiteral("Flagged")); + QVERIFY(config.problems().isEmpty()); +} + +void TestConfig::unknownStartupQueryFallsBackAndReports() +{ + // A name the user wrote that matches nothing is a problem: they asked for + // something and are not getting it. Startup still works, on the fallback. + QTemporaryDir dir; + Config config; + config.load(writeIni(dir, QStringLiteral( + "[general]\n" + "startup_query=Nonexistent\n" + "\n" + "[queries]\n" + "Inbox=tag:inbox\n"))); + + QCOMPARE(config.startupSavedQuery().name, QStringLiteral("Inbox")); + QCOMPARE(config.problems().size(), 1); + + // The built-in default naming a query the user never created is NOT a + // problem: they did not get it wrong, they simply have no Unread entry. + QTemporaryDir quiet; + Config silent; + silent.load(writeIni(quiet, QStringLiteral( + "[queries]\n" + "Inbox=tag:inbox\n"))); + + QCOMPARE(silent.startupSavedQuery().name, QStringLiteral("Inbox")); + QVERIFY(silent.problems().isEmpty()); +} + +void TestConfig::generalSectionKeysAreActuallyRead() +{ + // QSettings' INI backend treats a section literally named [general] as its + // own fallback section and strips the prefix, so a "general/<key>" lookup + // matches nothing. notmuch_config was read that way and had never worked. + QTemporaryDir dir; + Config config; + config.load(writeIni(dir, QStringLiteral( + "[general]\n" + "notmuch_config=/somewhere/notmuch-config\n" + "\n" + "[sync]\n" + "command=/bin/true\n"))); + + QCOMPARE(config.notmuchConfig(), + QStringLiteral("/somewhere/notmuch-config")); +} + +void TestConfig::messageZoomDefaultsAndValidates() +{ + // A QTemporaryDir per case, not one shared: writeIni() always uses the + // same file name, and QSettings caches by path, so a second load of the + // same path would return the first case's contents. + + // Absent: 1.0, silently. Nothing the user asked for is being ignored. + { + QTemporaryDir dir; + Config config; + config.load(writeIni(dir, QStringLiteral("[general]\n"))); + QCOMPARE(config.messageZoom(), 1.0); + QVERIFY(config.problems().isEmpty()); + } + + { + QTemporaryDir dir; + Config config; + config.load(writeIni(dir, QStringLiteral("[general]\n" + "message_zoom=1.25\n"))); + QCOMPARE(config.messageZoom(), 1.25); + QVERIFY(config.problems().isEmpty()); + } + + // Present but unparseable is a problem: the user asked for something and + // is not getting it, which is the line addProblem() draws. + { + QTemporaryDir dir; + Config config; + config.load(writeIni(dir, QStringLiteral("[general]\n" + "message_zoom=huge\n"))); + QCOMPARE(config.messageZoom(), 1.0); + QCOMPARE(config.problems().size(), 1); + } +} + QTEST_MAIN(TestConfig) #include "test_config.moc" diff --git a/tests/test_keymap.cpp b/tests/test_keymap.cpp index c81eeb0..0fb4f57 100644 --- a/tests/test_keymap.cpp +++ b/tests/test_keymap.cpp @@ -36,8 +36,28 @@ private slots: void userBindingWinsOverDefaultInMenus(); void defaultsDoNotCollide(); void everyDefaultIsAKnownAction(); + void everyDefaultParses(); }; +void TestKeyMap::everyDefaultParses() +{ + // A default that does not parse is a dead binding, the failure mode + // bareCapitalMatchesShiftedPress() covers for user-written keys. + // + // This deliberately does NOT try to decide which keys a keyboard can + // deliver. Whether a symbol needs Shift is a layout property, not a Qt + // one: Ctrl++ is exactly what the '+' key emits on an Italian layout and + // is unreachable on a US one, and QTest::keyClick() cannot reproduce + // either faithfully. A test asserting reachability from synthetic input + // would encode one layout's habits as a rule for all of them. + for (const auto &binding : KeyMap::defaultBindings()) { + const QKeySequence sequence = KeyMap::normalizeSequence(binding.first); + QVERIFY2(!sequence.isEmpty(), + qPrintable(QStringLiteral("default '%1' for %2 does not parse") + .arg(binding.first, binding.second))); + } +} + void TestKeyMap::defaultsAreLoaded() { KeyMap map; diff --git a/tests/test_mainwindow.cpp b/tests/test_mainwindow.cpp index 6bfa925..b4d9e4a 100644 --- a/tests/test_mainwindow.cpp +++ b/tests/test_mainwindow.cpp @@ -20,12 +20,15 @@ #include <QAction> #include <QDir> +#include <QFile> #include <QSettings> +#include <QStandardPaths> #include <QTemporaryDir> #include "config.h" #include "keymap.h" #include "mainwindow.h" +#include "messageview.h" /// MainWindow is mostly wiring, and the parts that need a real database are /// still verified manually. What is checked here is the action registry: the @@ -41,6 +44,9 @@ private slots: void configuredBindingReachesTheAction(); void cidPrefixesAreBangFree(); void cidPrefixesAreDistinctPerMessage(); + void uiStateIsNotWrittenIntoTheUserConfig(); + void uiStateSurvivesARestart(); + void missingUiStateLeavesTheDefaults(); }; void TestMainWindow::everyKnownActionIsRegistered() @@ -158,6 +164,64 @@ void TestMainWindow::cidPrefixesAreDistinctPerMessage() } } +void TestMainWindow::uiStateIsNotWrittenIntoTheUserConfig() +{ + // The config file is hand-edited and must never gain a base64 geometry + // blob, nor be rewritten on exit: QSettings preserves neither comments nor + // key order, so writing it would quietly destroy the user's formatting. + QVERIFY(MainWindow::uiStatePath() != Config::defaultPath()); + + // One qtmaildir component, not two. QStandardPaths::StateLocation appends + // both the organization and the application name, and here both are + // "qtmaildir", so using it nests the directory inside itself. + QCOMPARE(MainWindow::uiStatePath().count(QStringLiteral("/qtmaildir/")), 1); + QVERIFY(MainWindow::uiStatePath().endsWith( + QStringLiteral("/qtmaildir/uistate.conf"))); +} + +void TestMainWindow::uiStateSurvivesARestart() +{ + // Test mode redirects QStandardPaths at the process level, so the state + // file lands in a scratch directory rather than the real ~/.local/state. + QStandardPaths::setTestModeEnabled(true); + QFile::remove(MainWindow::uiStatePath()); + + const QSize resized(940, 620); + { + const Config config; + MainWindow window(config); + window.resize(resized); + window.findChild<MessageView *>()->setZoomFactor(1.4); + window.close(); // closeEvent() is what persists the state + } + + QVERIFY2(QFile::exists(MainWindow::uiStatePath()), + qPrintable(QStringLiteral("no state file at %1") + .arg(MainWindow::uiStatePath()))); + + const Config config; + MainWindow reopened(config); + QCOMPARE(reopened.size(), resized); + QCOMPARE(reopened.findChild<MessageView *>()->zoomFactor(), 1.4); + + QFile::remove(MainWindow::uiStatePath()); + QStandardPaths::setTestModeEnabled(false); +} + +void TestMainWindow::missingUiStateLeavesTheDefaults() +{ + // A restore that silently succeeded on an empty blob would give a + // zero-size window on first launch. Absent state must be a no-op. + QStandardPaths::setTestModeEnabled(true); + QFile::remove(MainWindow::uiStatePath()); + + const Config config; + MainWindow window(config); + QCOMPARE(window.size(), QSize(1200, 800)); + + QStandardPaths::setTestModeEnabled(false); +} + // Constructing a MainWindow needs a QApplication and a platform plugin. The // test has no display under ctest, so it runs offscreen unless the caller // asked for something else. diff --git a/tests/test_messageview.cpp b/tests/test_messageview.cpp index 0c3353d..4b6bc4c 100644 --- a/tests/test_messageview.cpp +++ b/tests/test_messageview.cpp @@ -36,6 +36,8 @@ private slots: void documentActuallyLoads(); void threadContentReachesThePage(); void dataUrlSubResourceStillBlocked(); + void zoomIsClampedToARenderableRange(); + void zoomSurvivesANewDocument(); private: QWebEngineView *webViewOf(MessageView *view) const @@ -175,5 +177,58 @@ void TestMessageView::dataUrlSubResourceStillBlocked() QVERIFY(text.contains(QStringLiteral("visible-text"))); } +void TestMessageView::zoomIsClampedToARenderableRange() +{ + // A factor outside the range leaves the pane unreadable, and the only way + // back is a menu entry the user can no longer read. A corrupt state file + // reaching setZoomFactor() must not be able to do that. + QCOMPARE(MessageView::clampZoom(100.0), MessageView::kMaxZoom); + QCOMPARE(MessageView::clampZoom(0.01), MessageView::kMinZoom); + + // A missing or non-numeric state value converts to 0.0, and a hand-edited + // one can hold NaN or an infinity. None of those may reach the web view. + QCOMPARE(MessageView::clampZoom(0.0), MessageView::kDefaultZoom); + QCOMPARE(MessageView::clampZoom(-2.0), MessageView::kDefaultZoom); + QCOMPARE(MessageView::clampZoom(qQNaN()), MessageView::kDefaultZoom); + QCOMPARE(MessageView::clampZoom(qInf()), MessageView::kDefaultZoom); + + // In-range values pass through untouched. + QCOMPARE(MessageView::clampZoom(1.4), 1.4); + + MessageView view; + view.setZoomFactor(50.0); + QCOMPARE(view.zoomFactor(), MessageView::kMaxZoom); +} + +void TestMessageView::zoomSurvivesANewDocument() +{ + // MainWindow persists whatever zoomFactor() reports and never reapplies it + // per render, which is only correct if the web view keeps the factor + // across setHtml(). Verified rather than assumed. + MessageView view; + QWebEngineView *web = webViewOf(&view); + QVERIFY(web); + + view.setZoomFactor(1.5); + + QSignalSpy loaded(web, &QWebEngineView::loadFinished); + + ParsedMessage message; + message.ok = true; + message.from = QStringLiteral("Sender <sender@example.org>"); + message.subject = QStringLiteral("Zoom"); + message.plainBody = QStringLiteral("body text"); + + ThreadRenderItem item; + item.message = message; + item.cidPrefix = QStringLiteral("m0"); + item.expanded = true; + + view.showThread({ item }); + QVERIFY(loaded.wait(15000)); + + QCOMPARE(view.zoomFactor(), 1.5); +} + QTEST_MAIN(TestMessageView) #include "test_messageview.moc" |
