using Avalonia.Controls; using DodoSSH.Client.App.Views; using DodoSSH.Client.Session; using DodoSSH.Client.Shell.ViewModels; namespace DodoSSH.Client.App.Layout.Tests; /// /// The update banner, measured at the width the window is allowed to shrink to. /// /// /// /// This is the control in the application least protected by anything else. It is a fixed-height row with /// two buttons on the right and a version string of unknown length in the middle, and it is not /// inside a — so the harness's scrolling exemption does not apply and both /// buttons are genuinely measured. The failure it exists to catch is RESTART NOW pushed off the right edge /// by a long version, which nobody would see until the day there was an update to install. /// /// /// It cannot be measured through MainWindow, which is why the banner is its own control: WebView2's /// adapter refuses the headless session's thread. See LayoutHarnessTests.WhyTheWindowItselfIsNeverShown. /// /// public sealed class UpdateBannerTests { private static CancellationToken Token => TestContext.Current.CancellationToken; /// /// A channel that reports an update is ready, so the banner has something to draw. /// /// /// Local to this suite rather than shared with DodoSSH.Client.App.Tests: what a layout test needs /// is a view model in a given state, and threading the other suite's fake through a project reference /// would couple two suites that otherwise share nothing. /// private sealed class ReadyChannel : IUpdateChannel { public bool IsSupported => true; // The desktop's answer, which is what this suite lays out. Nothing here applies anything. public bool ApplyingEndsTheProcess => true; public string CurrentVersion => "1.0.0"; public Task CheckAsync(CancellationToken cancellationToken) => Task.FromResult(new AvailableUpdate(LongVersion)); public Task DownloadAsync( AvailableUpdate update, IProgress progress, CancellationToken cancellationToken) { progress.Report(100); return Task.CompletedTask; } public void ApplyAndRestart(AvailableUpdate update) { } } /// /// A version longer than anything this project should ever tag, which is the point. /// /// /// MinVer produces exactly this shape on an untagged commit — 0.0.0-alpha.0.114 — and a release /// candidate would produce something close to it. A banner that only fits 1.2.3 would look fine /// in every screenshot and break on the first prerelease anybody actually shipped. /// private const string LongVersion = "10.20.30-release-candidate.4"; private static async Task ReadyViewModelAsync() { var directory = Path.Combine(Path.GetTempPath(), $"dodossh-banner-{Guid.CreateVersion7():N}"); var updates = new UpdateViewModel( new ReadyChannel(), new ClientSettingsStore(new ClientPaths(directory)), TimeProvider.System, () => 3, restart: _ => Task.CompletedTask); await updates.CheckOnceAsync(TestContext.Current.CancellationToken); if (Directory.Exists(directory)) { Directory.Delete(directory, recursive: true); } return updates; } [Fact] public async Task TheUpdateBannerFitsAtTheWindowsMinimumWidth() { var updates = await ReadyViewModelAsync(); await LayoutHarness.OnTheUiThreadAsync( () => { var banner = new UpdateBanner { DataContext = updates }; var window = LayoutHarness.HostAtMinimumSize( banner, LayoutHarness.MinimumWidth, LayoutHarness.UpdateBannerHeight); try { LayoutHarness.Unreachable(window).ShouldBeEmpty(); } finally { window.Close(); } }, Token); } /// /// Held to the number states, the same way the titlebar, /// tab strip and status bar are. A banner that grew with its contents would shrink the terminal by an /// amount nothing predicts, and would do it while somebody is reading it. /// [Fact] public async Task TheUpdateBannerIsTheHeightItSaysItIs() { var updates = await ReadyViewModelAsync(); await LayoutHarness.OnTheUiThreadAsync( () => { var banner = new UpdateBanner { DataContext = updates }; var window = LayoutHarness.HostAtMinimumSize( banner, LayoutHarness.MinimumWidth, LayoutHarness.UpdateBannerHeight); try { banner.Bounds.Height.ShouldBe(LayoutHarness.UpdateBannerHeight); } finally { window.Close(); } }, Token); } }