aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-14 18:25:03 +0200
committerDanilo M. <danix@danix.xyz>2026-08-14 18:25:03 +0200
commit86074f40121466798cdd64e07a00db2542543c52 (patch)
tree96517aeee0a8a6bb072a3e7d5e0eda7651423fba
parent1c479f2834533938d8055146c75fe7cbe79a8cd0 (diff)
downloadqtmaildir-86074f40121466798cdd64e07a00db2542543c52.tar.gz
qtmaildir-86074f40121466798cdd64e07a00db2542543c52.zip
fix(startup): raise the config-problem modal outside the constructor
showWarnings() did two separable things and one of them could not be reached from a test. It set the status label, which is harmless, and it raised a QMessageBox from the MainWindow constructor, which under the offscreen platform nothing can dismiss: the constructor never returned and the suite hung with no output, reading as an infrastructure failure rather than a test one. It splits in two. applyWarnings() keeps the status label and stays in the constructor. configProblems() returns the list, and main.cpp raises the dialog after show(), which also gives it a visible parent to sit on. The distinction between warnings and problems is preserved exactly: a keybinding being ignored interrupts startup, "no sync command configured" does not. The warning path now has its first test, using the config shape that caused the original hang. Mutation checked by putting the modal back in the constructor: the test times out at 124 rather than failing, which is the behaviour this removes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
-rw-r--r--src/main.cpp10
-rw-r--r--src/mainwindow.cpp17
-rw-r--r--src/mainwindow.h16
-rw-r--r--tests/test_mainwindow.cpp35
4 files changed, 69 insertions, 9 deletions
diff --git a/src/main.cpp b/src/main.cpp
index 231594f..2804196 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -97,5 +97,15 @@ int main(int argc, char *argv[])
MainWindow window(config);
window.show();
+ // After show(), and out here rather than inside the constructor. A modal
+ // raised from the constructor cannot be dismissed under the offscreen
+ // platform, so it hung the test suite with no output (item 84). Showing it
+ // here also gives the dialog a visible parent to sit on.
+ const QStringList problems = window.configProblems();
+ if (!problems.isEmpty()) {
+ QMessageBox::warning(&window, QObject::tr("Configuration problems"),
+ problems.join(QLatin1Char('\n')));
+ }
+
return app.exec();
}
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index bf6fc79..ee559fe 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -384,7 +384,10 @@ MainWindow::MainWindow(const Config &config, QWidget *parent)
// object name, so they must already exist or their position is dropped.
restoreUiState();
wireWorker();
- showWarnings();
+ // Sets the status label only. The modal that used to live here is raised
+ // by the caller after show(), because a modal in a constructor cannot be
+ // dismissed under the offscreen platform and hung the whole suite.
+ applyWarnings();
// No window-wide event filter: QAction shortcuts are dispatched before the
// focused widget sees the key, so they beat QAbstractItemView's
@@ -1688,7 +1691,7 @@ void MainWindow::runSearchFromPane(const QString &query,
runCurrentQuery();
}
-void MainWindow::showWarnings()
+void MainWindow::applyWarnings()
{
const QStringList warnings = m_config.warnings() + m_keyMap.warnings();
if (warnings.isEmpty())
@@ -1697,18 +1700,16 @@ void MainWindow::showWarnings()
// Non-fatal: the app runs degraded rather than refusing to start.
m_statusLabel->setText(
tr("%n configuration warning(s)", "", warnings.size()));
+}
+QStringList MainWindow::configProblems() const
+{
// Interrupt startup only for things that are actually wrong. Every KeyMap
// warning qualifies (each one means a binding the user wrote is being
// ignored), but a Config notice such as "no sync command configured" does
// not: nothing is broken, the feature is simply off, and a modal on every
// launch teaches the user to dismiss dialogs unread.
- const QStringList problems = m_config.problems() + m_keyMap.warnings();
- if (problems.isEmpty())
- return;
-
- QMessageBox::warning(this, tr("Configuration problems"),
- problems.join(QLatin1Char('\n')));
+ return m_config.problems() + m_keyMap.warnings();
}
void MainWindow::buildSavedQueryRow(QWidget *parent, QVBoxLayout *layout)
diff --git a/src/mainwindow.h b/src/mainwindow.h
index b41f75d..d844f1e 100644
--- a/src/mainwindow.h
+++ b/src/mainwindow.h
@@ -75,6 +75,18 @@ public:
/// really registered.
QStringList registeredActionNames() const;
+ /// Configuration problems worth interrupting startup for, empty when there
+ /// are none. A keybinding the user wrote and that is being ignored counts;
+ /// a notice such as "no sync command configured" does not.
+ ///
+ /// **Returned rather than shown, and that is the point.** This used to
+ /// raise a `QMessageBox` from the CONSTRUCTOR. A modal cannot be dismissed
+ /// under the offscreen platform, so `MainWindow` never finished
+ /// constructing and the whole suite hung with no output, which reads as an
+ /// infrastructure failure rather than a test one (item 84). The caller
+ /// raises the dialog after show(); a test asserts on the list.
+ QStringList configProblems() const;
+
/// The thread currently shown in the message pane, empty when it is blank.
///
/// Empty is what "the pane is blanked" means internally: a late-arriving
@@ -460,7 +472,9 @@ private:
QList<HtmlBuilder::PlaceholderHelper> placeholderHelpers() const;
- void showWarnings();
+ /// Puts the warning count in the status bar. Nothing modal: see
+ /// configProblems() for why the dialog is not raised here.
+ void applyWarnings();
void showShortcutReference();
void showAbout();
diff --git a/tests/test_mainwindow.cpp b/tests/test_mainwindow.cpp
index 0bf8925..be3a28f 100644
--- a/tests/test_mainwindow.cpp
+++ b/tests/test_mainwindow.cpp
@@ -91,6 +91,7 @@ private slots:
void aSearchFromThePaneReplacesTheQuery();
void aSearchFromThePaneCanNarrowTheQuery();
void narrowingAnEmptyQueryBarIsAPlainSearch();
+ void aMalformedAccountIsReportedWithoutBlockingTheConstructor();
void autoSyncIsNotArmedWhenDisabledOrWithNothingPending();
void autoSyncSkipsWhileABackgroundSyncIsRunning();
void aSuccessfulSyncRefreshesRatherThanRerunningTheQuery();
@@ -6105,4 +6106,38 @@ void TestMainWindow::renamingReplacesRatherThanDuplicating()
QCOMPARE(savedQueryButtonLabels(window), QStringList{ QStringLiteral("New") });
}
+void TestMainWindow::aMalformedAccountIsReportedWithoutBlockingTheConstructor()
+{
+ // The exact shape that hung the suite on 2026-08-14: an account section
+ // carrying `sent=` and no `maildir=`. Config::load handles it correctly,
+ // recording a problem and carrying on, but showWarnings() then raised a
+ // modal FROM THE CONSTRUCTOR, which nothing can dismiss under the
+ // offscreen platform, so MainWindow never finished constructing.
+ //
+ // Constructing the window at all is therefore half the assertion: if the
+ // modal comes back, this test does not fail, it HANGS.
+ QTemporaryDir dir;
+ QVERIFY(dir.isValid());
+
+ const QString path = dir.filePath(QStringLiteral("qtmaildir.conf"));
+ {
+ QFile file(path);
+ QVERIFY(file.open(QIODevice::WriteOnly | QIODevice::Text));
+ QTextStream out(&file);
+ out << "[account.one]\n"
+ << "sent=Sent\n";
+ }
+
+ Config config;
+ config.load(path);
+
+ MainWindow window(config);
+
+ // The problem is reported rather than swallowed, and it names the account.
+ const QStringList problems = window.configProblems();
+ QVERIFY2(!problems.isEmpty(),
+ "a malformed account produced no problem to report");
+ QVERIFY(problems.join(QLatin1Char('\n')).contains(QStringLiteral("one")));
+}
+
#include "test_mainwindow.moc"