From 334b510e2673a6ab3875ffa7a4c5b3b2dd09a369 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Fri, 7 Aug 2026 17:51:52 +0200 Subject: feat(ui): fill the blank message pane with a branded placeholder An empty right pane said nothing, and multi-select made it a routine sight. It now carries the wordmark, thread counts that run their query when clicked, and a sync line that appears only when something needs attention. Rendered into the existing web view as a third document shape, so there is one document path and one set of security rules. The brand palette is a deliberate exception to deriving colours from the desktop theme, since a logo is brand rather than chrome; the theme still picks which of the two sets is used. Counts refresh when the pane is about to show rather than in the background: one goes stale the moment a tag is edited, and refreshing one nobody is looking at is work for nothing. A generation counter discards a superseded reply, and a late answer cannot repaint over an opened thread. The helper lines are real links because JavaScript is off in this profile. The handler is gated on the placeholder actually being displayed, so the same URL inside a message body is dropped: a stranger's mail must not drive the thread list, even to run a harmless query. Three defects found while building, all silent: - Every CSS percentage was invalid. QString::arg does not collapse "%%" into "%", so the document carried "50%%" and the browser dropped each declaration holding one, disabling the mask, the glow and both radial gradients while still rendering something plausible. Substitution is by named token now, which cannot collide with a percent sign. - A geometry probe endorsed the layout while that was live, because it measured only properties without percentages. - The font test passed against a build with one face missing, since the other satisfied both of its checks on its own. The mockup's light values needed correcting against a real pane: the grid vanished at a 2% luminance step on white, and the glow subtracts light there rather than adding it, washing the pane. Strength only, not hue. --- tests/test_htmlbuilder.cpp | 171 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) (limited to 'tests/test_htmlbuilder.cpp') diff --git a/tests/test_htmlbuilder.cpp b/tests/test_htmlbuilder.cpp index 7fa45a2..fdb57c5 100644 --- a/tests/test_htmlbuilder.cpp +++ b/tests/test_htmlbuilder.cpp @@ -48,6 +48,15 @@ private slots: // Theming. void aDarkPaletteProducesADarkDocument(); void everyColourComesFromThePalette(); + + // The placeholder pane (item 30). + void placeholderPicksTheBrandSetFromTheDesktopTheme(); + void placeholderHelperBecomesALinkOnItsQuery(); + void placeholderHelperWithoutAQueryIsNotALink(); + void placeholderEscapesHelperText(); + void placeholderEmbedsItsFontsRatherThanFetchingThem(); + void placeholderReferencesNoRemoteResource(); + void placeholderStyleHasNoUnsubstitutedTokens(); void theBodyAlwaysGetsABackground(); void aSendersOwnHtmlIsNotRecoloured(); }; @@ -357,6 +366,168 @@ void TestHtmlBuilder::everyColourComesFromThePalette() } } +void TestHtmlBuilder::placeholderPicksTheBrandSetFromTheDesktopTheme() +{ + // The brand colours are fixed, deliberately: this is the one place where + // the desktop palette does NOT supply the values. What the desktop decides + // is which of the two sets is used, and getting that backwards is the + // failure the item warns about, a light lockup on a dark desktop. + const HtmlBuilder::BrandPalette dark = HtmlBuilder::brandPaletteFrom( + makeTestPalette(QColor(0x1a, 0x1a, 0x1a), QColor(0xee, 0xee, 0xee))); + const HtmlBuilder::BrandPalette light = HtmlBuilder::brandPaletteFrom( + makeTestPalette(QColor(0xff, 0xff, 0xff), QColor(0x11, 0x11, 0x11))); + + QCOMPARE(dark.background.name(), QStringLiteral("#060b10")); + QCOMPARE(light.background.name(), QStringLiteral("#ffffff")); + + // Not merely different: the right way round. A set whose background is + // darker than its text is the dark set, whichever values it holds. + QVERIFY(dark.background.lightnessF() < dark.title.lightnessF()); + QVERIFY(light.background.lightnessF() > light.title.lightnessF()); +} + +void TestHtmlBuilder::placeholderHelperBecomesALinkOnItsQuery() +{ + // JavaScript is off in this profile, so a helper can only be actionable by + // being a real link that the page's navigation handler intercepts. + const QString html = HtmlBuilder::buildPlaceholder( + { { QStringLiteral("12 unread"), QStringLiteral("tag:unread") } }, + QStringLiteral("0.10.0"), + HtmlBuilder::brandPaletteFrom(QPalette())); + + QVERIFY(html.contains(QStringLiteral("href=\"qtmaildir-query:tag%3Aunread\""))); + QVERIFY(html.contains(QStringLiteral("12 unread"))); +} + +void TestHtmlBuilder::placeholderHelperWithoutAQueryIsNotALink() +{ + // The sync line reports a state rather than naming a search, so clicking it + // must not run an empty query and wipe the thread list. + const QString html = HtmlBuilder::buildPlaceholder( + { { QStringLiteral("3 edits waiting to sync"), QString() } }, + QStringLiteral("0.10.0"), + HtmlBuilder::brandPaletteFrom(QPalette())); + + QVERIFY(html.contains(QStringLiteral("3 edits waiting to sync"))); + QVERIFY(!html.contains(QStringLiteral("qtmaildir-query:"))); +} + +void TestHtmlBuilder::placeholderEscapesHelperText() +{ + // A helper label carries a count this code produced, but the query half is + // built from configuration and a saved query is user-written. Neither may + // reach the document unescaped, and the query is doubly encoded: percent + // for the URL, then HTML for the attribute. + const QString html = HtmlBuilder::buildPlaceholder( + { { QStringLiteral(""), + QStringLiteral("tag:\"a\">