diff options
| author | Danilo M. <danix@danix.xyz> | 2026-08-25 10:13:20 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-08-25 10:13:20 +0200 |
| commit | 07c36ce617512bb9514e604b614e95a15c390b04 (patch) | |
| tree | c933635e83f55cda94e4b1a9457a28cebefc3ed4 /tests/test_composewindow.cpp | |
| parent | 01ea9e9d7df04dc771430e4c378202b8ef37b8db (diff) | |
| download | qtmaildir-07c36ce617512bb9514e604b614e95a15c390b04.tar.gz qtmaildir-07c36ce617512bb9514e604b614e95a15c390b04.zip | |
feat(compose): a menu bar on the composer
File, Edit and Format, to the scope the user chose. Save draft (Ctrl+S)
is the only new action: saveDraftNow() was reachable from the autosave
timer, the send path and closeEvent, so there was no way for the user to
ask for a save. It routes through that same function, which is what
emits draftSaved for item 158's indexing, reports through item 160's
status bar and raises the failure banner; a second write path would have
to repeat all three.
The menus show the toolbar's own QAction objects rather than copies, as
item 140 required for the message pane's bar. Two needed hand-building.
The HTML toggle is a QToolButton and cannot go in a menu, so a checkable
twin mirrors it in both directions, since a menu entry that only follows
the button is half a control. The signature entry takes the switch's own
QMenu pointer, because that menu is rebuilt whenever the signatures
change and copied entries would go stale.
Edit's entries drive QPlainTextEdit and follow its own undoAvailable and
copyAvailable, so a greyed entry tells the truth about what pressing it
would do.
theMenuBarReachesEveryComposerAction() is item 132's reachability rule
applied to the composer: it walks the real menu bar and collects the
composer's actions with findChildren, so an action added to the toolbar
and forgotten in the menus fails without the test being touched. It
skips actions owning a submenu, since Qt emits no triggered for those.
The composer's actions stay out of KeyMap, per item 148: they are
parented to this window, so they are WindowShortcuts dispatched to the
active composer and the main window's namespace is untouched.
lrelease reports 496 finished, 0 unfinished.
Closes item 161.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UUQS6n3cmsFrsjCNmwNtf8
Diffstat (limited to 'tests/test_composewindow.cpp')
| -rw-r--r-- | tests/test_composewindow.cpp | 165 |
1 files changed, 165 insertions, 0 deletions
diff --git a/tests/test_composewindow.cpp b/tests/test_composewindow.cpp index 68ec4d4..779d48c 100644 --- a/tests/test_composewindow.cpp +++ b/tests/test_composewindow.cpp @@ -24,6 +24,10 @@ #include <QPlainTextEdit> #include <QSignalSpy> #include <QLabel> +#include <QMenuBar> +#include <QToolBar> +#include <QSet> +#include <functional> #include <QRegularExpression> #include <QTemporaryDir> #include <QTimer> @@ -56,6 +60,10 @@ private slots: void aSavedDraftReportsItAndClearsTheCue(); void aSentMessageLeavesNoUnsavedCue(); void onlyTheSetterWritesTheDirtyFlag(); + void theMenuBarReachesEveryComposerAction(); + void saveDraftWritesAndReports(); + void theMenusReuseTheToolbarActions(); + void theHtmlMenuItemTracksTheToolbarButton(); void theAgeLineFollowsTheClock(); private: @@ -643,5 +651,162 @@ void TestComposeWindow::theAgeLineFollowsTheClock() "the age line must move as the clock does"); } +/// Item 132's rule for the main window, applied to the composer: an action +/// nobody can find in a menu is reachable only by a chord the user has to +/// know. Walks the real menu bar rather than a list, so an action added to +/// the toolbar and forgotten in the menus fails here. +void TestComposeWindow::theMenuBarReachesEveryComposerAction() +{ + const Config config = configWithDrafts(); + + ComposeContext context; + context.kind = ComposeContext::Kind::New; + context.accountKey = QStringLiteral("work"); + + ComposeWindow window(context, config, m_dir->path()); + + QMenuBar *bar = window.menuBar(); + QVERIFY2(bar, "the composer has no menu bar"); + + // Every action the menus reach, submenus included. + QSet<QString> reachable; + std::function<void(QMenu *)> walk = [&](QMenu *menu) { + const QList<QAction *> actions = menu->actions(); + for (QAction *action : actions) { + if (QMenu *sub = action->menu()) { + // An action owning a submenu is not itself reachable: Qt + // emits no triggered for it, as CLAUDE.md records. + walk(sub); + continue; + } + if (action->isSeparator()) + continue; + if (!action->objectName().isEmpty()) + reachable.insert(action->objectName()); + } + }; + const QList<QAction *> top = bar->actions(); + for (QAction *action : top) { + QVERIFY2(action->menu(), "a top-level menu bar entry with no menu"); + walk(action->menu()); + } + + // Every named action the composer owns. findChildren, so an action added + // later is picked up without touching this list. + const QList<QAction *> owned = window.findChildren<QAction *>(); + QStringList missing; + for (QAction *action : owned) { + const QString name = action->objectName(); + if (name.isEmpty()) + continue; + // The signature entries are built from the files on disk and named + // per signature; the switch itself is what a menu offers. + if (name.startsWith(QStringLiteral("signature_choice"))) + continue; + if (!reachable.contains(name)) + missing.append(name); + } + + QVERIFY2(missing.isEmpty(), + qPrintable(QStringLiteral("not reachable from any menu: %1") + .arg(missing.join(QStringLiteral(", "))))); +} + +/// The action item 161 adds. Save draft did not exist at all: saveDraftNow() +/// was reachable only from the timer, the send path and closeEvent, so the +/// user could not ask for a save. +void TestComposeWindow::saveDraftWritesAndReports() +{ + const Config config = configWithDrafts(); + + ComposeContext context; + context.kind = ComposeContext::Kind::New; + context.accountKey = QStringLiteral("work"); + + ComposeWindow window(context, config, m_dir->path()); + auto *body = window.findChild<QPlainTextEdit *>(QStringLiteral("body")); + QVERIFY(body); + body->setPlainText(QStringLiteral("Saved by hand.")); + + auto *save = window.findChild<QAction *>(QStringLiteral("compose_save")); + QVERIFY2(save, "there is no Save draft action"); + QCOMPARE(save->shortcut(), QKeySequence(QStringLiteral("Ctrl+S"))); + + QSignalSpy saved(&window, &ComposeWindow::draftSaved); + save->trigger(); + + QCOMPARE(saved.size(), 1); + + // Routed through saveDraftNow(), so item 160's reporting comes free. A + // second write path would have to repeat it, and would be the ghost-file + // bug item 158 fixed. + auto *age = window.findChild<QLabel *>(QStringLiteral("draftAge")); + QVERIFY(age); + QVERIFY2(!age->text().isEmpty(), "a manual save must report like an autosave"); + QVERIFY2(!window.isWindowModified(), "a manual save must clear the marker"); +} + +/// The same QAction objects, shown twice over, exactly as item 140 required +/// for the message pane's bar. A copy would drift: an enablement change or a +/// new shortcut would reach one surface and not the other. +void TestComposeWindow::theMenusReuseTheToolbarActions() +{ + const Config config = configWithDrafts(); + + ComposeContext context; + context.kind = ComposeContext::Kind::New; + context.accountKey = QStringLiteral("work"); + + ComposeWindow window(context, config, m_dir->path()); + + auto *bold = window.findChild<QAction *>(QStringLiteral("format_bold")); + auto *toolbar = window.findChild<QToolBar *>(QStringLiteral("formatToolbar")); + QVERIFY(bold); + QVERIFY(toolbar); + QVERIFY2(toolbar->actions().contains(bold), + "Bold left the formatting toolbar"); + + QMenu *format = nullptr; + const QList<QAction *> top = window.menuBar()->actions(); + for (QAction *action : top) { + if (action->menu() && action->menu()->actions().contains(bold)) + format = action->menu(); + } + QVERIFY2(format, "Bold is not in any menu"); + + // The pointer itself, not a namesake. + QVERIFY2(format->actions().contains(bold), + "the menu holds a copy of Bold rather than the action itself"); +} + +/// The HTML toggle is a QToolButton, not a QAction, so a menu entry for it +/// has to be built and kept in step by hand. Both directions: a menu that +/// only follows the button is half a control. +void TestComposeWindow::theHtmlMenuItemTracksTheToolbarButton() +{ + const Config config = configWithDrafts(); + + ComposeContext context; + context.kind = ComposeContext::Kind::New; + context.accountKey = QStringLiteral("work"); + + ComposeWindow window(context, config, m_dir->path()); + + auto *button = window.findChild<QToolButton *>(QStringLiteral("sendHtml")); + auto *item = window.findChild<QAction *>(QStringLiteral("compose_send_html")); + QVERIFY(button); + QVERIFY2(item, "there is no menu entry for the HTML toggle"); + QVERIFY(item->isCheckable()); + + const bool initial = button->isChecked(); + QCOMPARE(item->isChecked(), initial); + + button->setChecked(!initial); + QCOMPARE(item->isChecked(), !initial); + + item->setChecked(initial); + QCOMPARE(button->isChecked(), initial); +} + QTEST_MAIN(TestComposeWindow) #include "test_composewindow.moc" |
