summaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/test_config.cpp123
-rw-r--r--tests/test_keymap.cpp20
-rw-r--r--tests/test_mainwindow.cpp64
-rw-r--r--tests/test_messageview.cpp55
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"