aboutsummaryrefslogtreecommitdiffstats
path: root/tests/test_composewindow.cpp
AgeCommit message (Collapse)AuthorFilesLines
21 hoursfeat: forward an HTML message with its formattingHEADmasterDanilo M.1-0/+199
Item 171. A forward carried only the plain-text version of the original, so formatting was lost; and an original with no plain-text part at all (30 of 342 sampled inbox messages, ~9%) forwarded as an empty quote with its content silently gone. A forward now sends ONE part chosen by the Send-as-HTML toggle: the original's markup when on, the text quote when off. Not a multipart/alternative, at the user's decision: a forward's shape is already decided by that toggle, and sending both hands the choice to the recipient's client. The toggle is honoured even for an HTML-only original, which then forwards as a text fallback. HtmlSanitiser strips remote content from the forwarded markup, checked by default with a per-forward opt-out. This is the security-critical part: the markup leaves this process and is rendered by the recipient's client, where none of MessageView's protections apply, so forwarding a tracking pixel forwards the tracking. It is an ALLOW-LIST, unlike HtmlBuilder::namespaceCids(), because a missed rewrite is a broken image while a missed strip is a beacon reaching the recipient. An HTML forward does not seed a text quote into the editor. The first build did, then subtracted it when building the HTML part, so the user could edit a quote whose edits were discarded; what the composer shows must be what gets sent. The forwarded message appears in a read-only pane beside the editor instead, a QSplitter at 60/40 with a toggle in the Format menu. A plain forward is unchanged. ComposeContextBuilder::quoteBody() renders htmlBody down to text when there is no plain part, so the plain path never emits an empty quote. Design in docs/superpowers/specs/2026-08-27-forward-html-design.md. Two tests repaired for the splitter: the 60/40 assertion reads stretch factors rather than pixels, since the offscreen platform gives the splitter no width and reports 49/49 whatever the code asks; and theComposerSplitsItsToolbarByScope looked for the body directly in the composer's column. Not yet hand-tested in this arrangement. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AtUzfNjMD8fiYfamDd3ywW
23 hoursfix: flag a saved draft seen so it is not tagged unreadDanilo M.1-0/+54
DraftStore::write() was called with "D", and it uses the flag string verbatim, so every draft this application wrote landed as :2,D. With maildir.synchronize_flags on, notmuch tags any message lacking the S flag `unread`, and a draft the user authored is seen by definition. The symptom heals itself: the next sync of that folder round-trips the file, adds S, and the tag goes away. Only the newest draft in a folder that has not synced since shows it, which is why it read as intermittent and why measuring an older draft finds nothing wrong. TestComposeWindow::aSavedDraftIsFlaggedSeen() asserts both flags on the written filename, verified failing first against "D". TestMainWindow::anAutosaveWritesADraftAndClearsTheDirtyFlag() asserted endsWith(":2,D"), pinning the whole flag set where its own comment said the point was the draft flag "not left bare", so it failed against the corrected behaviour. It checks for D within the flag set now. Also reconciles the backlog with the user's notes: records the forwarded-HTML defect as item 171, closes item 169 (shipped last session, its row still read open and its section was still in the open file), and records this fix as item 172. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AtUzfNjMD8fiYfamDd3ywW
3 daysfeat(compose): a menu bar on the composerDanilo M.1-0/+165
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
3 daysfeat(compose): report autosave state in a status barDanilo M.1-0/+196
Autosave worked and said nothing on success. The only feedback was m_banner, which is the failure channel and whose persistence is load-bearing for the quit path, so success got its own channel rather than sharing one. The fix is a funnel, not a label. m_dirty had seven writers, four of which clear it and only two of those are a save: the constructor clears it because seeding is not an edit, and the send handler clears it because the message is gone. A cue hung off saveDraftNow() would have been silently wrong in both. setDirty() is the only writer now, and it refreshes the status cue and setWindowModified() together so neither display can drift from the flag. The age line needs a tick of its own, since it moves with no edit to drive it. Five seconds against a label that reads in tens of them. Two defects found by probing rather than by reading. The %n plural rendered as "2 minute(s) ago" for every English user, because Qt picks a plural form only when a translation supplies the forms and there is no English .ts; it uses %1 and "min" now, which Italian substitutes identically. And the status mark was inside the translatable string, where a translator could drop it; it is concatenated outside tr(). Presentation reworked after the user looked at it. The first version reused item 151's yellow ribbon treatment, which reads as a misplaced widget on a bare status label rather than as a warning, and put both labels in the permanent widget area, which is the right-hand tray. They are ordinary status text on the left now. onlyTheSetterWritesTheDirtyFlag() asserts the funnel structurally, by reading composewindow.cpp: the first test for the send path called markClean() directly and a mutation restoring a direct assignment left the whole suite green. Four mutations now fail. The suite still cannot see the presentation, which is why that half needed a hand test. lrelease reports 487 finished, 0 unfinished. Closes item 160, and unblocks 161. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UUQS6n3cmsFrsjCNmwNtf8
4 daysfix(drafts): index a saved draft so it appears without a syncDanilo M.1-0/+54
Autosave writes the draft to the Maildir drafts folder and stops, while the Drafts view is a notmuch path: query, so a freshly saved draft was invisible until notmuch new ran. saveDraftNow() now emits draftSaved, and MainWindow connects it to a new NotmuchWorker::indexDraftFile() that indexes the one file the way moveMessages() does, with the previous revision removed so a rewrite leaves no ghost. The send path unlinks a draft that was indexed while being composed, so draftRemoved -> removeIndexedFile() drops its entry too. Measured: notmuch_database_index_file assigns NO tags (unlike notmuch new, which adds draft inbox unread), so no tag-stripping is needed and the draft cannot leak into a tag:inbox view. Item 158.
4 daysfix(compose): a resumed draft does not re-seed on a From: changeDanilo M.1-0/+58
A resumed draft kept m_signatureChosen false, so a From: change re-seeded the signature and rewrote what the user had saved, inserting the new account's where the saved block no longer matched a known file. The draft is the user's deliberate prior state and must not follow a From: change, so the draft branch marks it chosen.
4 daysfeat(compose): the signature follows the account until it is chosenDanilo M.1-0/+119
A From: change re-seeds the signature from the newly selected account, and stops doing so the moment the user picks one from the switch. Re-seeding unconditionally is the one behaviour that can silently discard a deliberate choice made a moment earlier; this is the shape send_html already uses. seededSignatureName() reads the combo rather than the context, which records where the composer opened and does not follow a change to it. Part of item 152.
4 daysfeat(compose): a signature switch on the editor barDanilo M.1-0/+220
A QToolButton with a checkable menu at the right end of the editor bar, where item 142 put the controls of the editor. Not registered in KeyMap: parented to the composer like the formatting actions, so its scope is this window. The signature is applied through a QTextCursor rather than setPlainText(), which destroys the undo stack, and the seeded one is cleared from that stack for the reason the seeded quote already is: one Ctrl+Z must not wipe content the user never typed. A resumed draft seeds nothing. Its body already carries the signature it was written with, and seeding again would put a second one on a message written once. Part of item 152.