using System.Globalization; using Avalonia; using Avalonia.Controls; using Avalonia.Layout; using Avalonia.VisualTree; namespace DodoSSH.Client.App.Layout.Tests; /// /// The three button shapes centre their caption inside a button taller than the caption. /// /// /// /// ◆ THE DEFECT THIS EXISTS FOR was read as a height problem and was not one. App.axaml's /// Button.ghost, Button.accent, Button.danger rule set VerticalAlignment — where the button /// sits in its parent — and never VerticalContentAlignment, where the caption sits in the button. /// Avalonia's default for the second is Stretch, so on any of these given a fixed Height the /// content presenter stretched the caption's to the whole content box, and a /// draws its line at the TOP of its bounds. The hosts toolbar's three 40-pixel /// buttons measured 9 pixels above the ink and 20 below it. Every box was the height it declared, which is /// exactly why it read as one being wrong: nothing was mis-sized, the labels sat in the top third. /// /// /// A bare rather than a screen, because Application.Styles is global — the same /// property leans on — so this measures the style rule itself rather than /// one of the hundred-odd places it lands. A screen-level test would pin one toolbar and leave the dialogs, /// the drawer's Save/Cancel pair and the import screen's buttons uncovered by the thing that fixed them all. /// /// /// ◆ THE LINE BOX IS MEASURED, NOT THE TextBlock's ARRANGED BOUNDS, and the difference is the whole test. /// A stretched caption's fill the content box, so they are symmetrical about /// the button's middle under the defect just as they are under the fix — the first draft of this suite /// asserted on them, passed on both, and was caught only by /// . What actually moves is where the line sits /// INSIDE those bounds: draws at the top of whatever it is given, so the ink is /// centred only when the box it is drawn in is its own line box. /// /// /// The gaps are compared to each other rather than to a number. What the caption's own line box measures is /// a property of JetBrains Mono at whichever size the caller set — 11.5 by default and 13.5 on the primary /// action — so an absolute expectation would be a font metric written down in a test file, and it would move /// the day the face does. "Centred" survives both. /// /// public sealed class ButtonCaptionTests { /// Taller than any caption these carry, which is the condition that exposes the defect. /// /// The hosts and keychain toolbars' own number. A button left to size itself cannot show this at all: /// its content box is its caption's line box exactly, so Stretch and Center agree and a /// test written against one would pass under either. /// private const double FixedHeight = 40; /// One pixel, for a content box whose odd leftover cannot be halved evenly. private const double Tolerance = 1; private static CancellationToken Token => TestContext.Current.CancellationToken; [Theory] [InlineData("ghost")] [InlineData("accent")] [InlineData("danger")] public async Task ACaptionIsCentredInAButtonTallerThanItself(string shape) { await MeasureAsync( shape, alignment: null, (above, below) => Math.Abs(above - below).ShouldBeLessThanOrEqualTo( Tolerance, string.Create( CultureInfo.InvariantCulture, $"Button.{shape} left {above:0.#} above the caption and {below:0.#} below it."))); } /// /// The instrument's own calibration: the check fails on the arrangement the fix replaced. /// /// /// The same practice states for the clipping harness, and it earns its /// place here for the same reason. Everything above passes if VerticalContentAlignment is Center /// AND it passes if the caption happens to fill its box, so without this a rule quietly reverted to /// Stretch would have to be caught by eye again. Stretch is set inline here rather than by editing the /// style sheet, because Application.Styles is global and a test that mutated it would be changing /// every other test in the assembly out from under itself. /// [Fact] public async Task AStretchedCaption_IsNotCentred_AndIsCaught() { await MeasureAsync( "ghost", VerticalAlignment.Stretch, (above, below) => (below - above).ShouldBeGreaterThan( Tolerance, "the caption should sit high, which is the defect this suite was written for")); } private static Task MeasureAsync( string shape, VerticalAlignment? alignment, Action assert) => LayoutHarness.OnTheUiThreadAsync( () => { var button = new Button { Content = "Group ▾", Height = FixedHeight }; button.Classes.Add(shape); if (alignment is { } forced) { button.VerticalContentAlignment = forced; } var window = LayoutHarness.HostAtMinimumSize( button, LayoutHarness.MinimumWidth, LayoutHarness.MinimumHeight); try { var caption = button.GetVisualDescendants().OfType().Single(); var origin = caption.TranslatePoint(default, button); origin.ShouldNotBeNull("the caption is not in the button's visual tree"); // The laid-out line rather than caption.Bounds.Height, which under Stretch is the // content box and so is symmetrical whether or not the ink in it is — see the remark on // the class. var line = caption.TextLayout.Height; var above = origin.Value.Y; var below = button.Bounds.Height - above - line; assert(above, below); } finally { window.Close(); } }, Token); }