Public Access
Merge branch 'claude/windows-update-bar-buttons-b19b2d'
This commit is contained in:
@@ -376,12 +376,25 @@
|
|||||||
buttons and a version string of unknown length is exactly the shape that arranges one of them off the
|
buttons and a version string of unknown length is exactly the shape that arranges one of them off the
|
||||||
edge. See UpdateBanner.axaml.
|
edge. See UpdateBanner.axaml.
|
||||||
|
|
||||||
|
◆ THE DATA CONTEXT IS SET HERE, and the banner did nothing at all until it was.
|
||||||
|
|
||||||
|
Unlike the titlebar and the status bar, which are typed to this window's own view model and inherit its
|
||||||
|
context, the banner is typed to UpdateViewModel — it is one screen's control and its layout suite hosts
|
||||||
|
it over that view model alone. Inheriting the shell's context instead left every compiled binding
|
||||||
|
inside it resolving against the wrong type and failing silently: no headline, and both Commands null,
|
||||||
|
so the strip appeared, hovered and pressed like a real banner and neither button did anything.
|
||||||
|
|
||||||
|
IsVisible is unqualified because the context is set on this same element, which resolves it against
|
||||||
|
UpdateViewModel too — the rule the page area's wrappers above are wrapped for. It needs no wrapper: the
|
||||||
|
flag it binds is the banner's own, unlike IsHostsScreen and its siblings, which belong to the shell.
|
||||||
|
|
||||||
FallbackValue, for the reason the WebView and the connecting card carry one: a compiled binding with
|
FallbackValue, for the reason the WebView and the connecting card carry one: a compiled binding with
|
||||||
no DataContext yields UnsetValue, IsVisible falls back to true, and the previewer would show a banner
|
no DataContext yields UnsetValue, IsVisible falls back to true, and the previewer would show a banner
|
||||||
announcing an update that does not exist.
|
announcing an update that does not exist.
|
||||||
-->
|
-->
|
||||||
<views:UpdateBanner Grid.Row="2"
|
<views:UpdateBanner Grid.Row="2"
|
||||||
IsVisible="{Binding Updates.IsBannerShowing, FallbackValue=False}" />
|
DataContext="{Binding Updates}"
|
||||||
|
IsVisible="{Binding IsBannerShowing, FallbackValue=False}" />
|
||||||
|
|
||||||
<views:StatusBar Grid.Row="3" />
|
<views:StatusBar Grid.Row="3" />
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.LogicalTree;
|
||||||
using DodoSSH.Client.App.Views;
|
using DodoSSH.Client.App.Views;
|
||||||
using DodoSSH.Client.Session;
|
using DodoSSH.Client.Session;
|
||||||
using DodoSSH.Client.Shell.ViewModels;
|
using DodoSSH.Client.Shell.ViewModels;
|
||||||
|
using DodoSSH.Client.Ssh;
|
||||||
|
using DodoSSH.Client.Storage;
|
||||||
|
using DodoSSH.Client.Terminal;
|
||||||
|
using NSubstitute;
|
||||||
|
|
||||||
namespace DodoSSH.Client.App.Layout.Tests;
|
namespace DodoSSH.Client.App.Layout.Tests;
|
||||||
|
|
||||||
@@ -145,4 +150,93 @@ public sealed class UpdateBannerTests
|
|||||||
},
|
},
|
||||||
Token);
|
Token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The banner in the window that shows it, rather than in a host window this suite built.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// The two tests above hand the control an <see cref="UpdateViewModel"/> themselves — which is precisely
|
||||||
|
/// what the window did not do. The banner is the one control here typed to a screen's view model instead
|
||||||
|
/// of to the shell's, and it was dropped into <c>MainWindow</c> with no data context at all, so it
|
||||||
|
/// inherited the shell's: every compiled binding inside it then resolved against the wrong type and
|
||||||
|
/// failed silently. The strip appeared, with no headline and both <c>Command</c>s null, and hovered and
|
||||||
|
/// depressed like a working banner while neither button did anything.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Nothing else could have caught it. A layout test supplies the context it is measuring, and the shell
|
||||||
|
/// suite has no visual tree — its own project file says as much: it does not cover whether the XAML binds
|
||||||
|
/// to the right names.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// The window is constructed and never shown, for the reason
|
||||||
|
/// <see cref="LayoutHarnessTests.WhyTheWindowItselfIsNeverShown"/> gives, and it does not need to be: a
|
||||||
|
/// data context propagates and a binding resolves when the context is set, not when the tree is measured.
|
||||||
|
/// So this asserts the wiring and leaves every question of size to the two tests above.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
[Fact]
|
||||||
|
public async Task TheWindowHandsTheBannerTheUpdateViewModel()
|
||||||
|
{
|
||||||
|
var directory = Path.Combine(Path.GetTempPath(), $"dodossh-banner-shell-{Guid.CreateVersion7():N}");
|
||||||
|
|
||||||
|
using var caches = ClientCacheFactory.ForMemory($"banner-{Guid.CreateVersion7():N}");
|
||||||
|
|
||||||
|
await using var workspace = new TerminalWorkspace(
|
||||||
|
new InMemoryTerminalAssetProvider(new Dictionary<string, TerminalAsset>(StringComparer.Ordinal)),
|
||||||
|
Substitute.For<ISshConnectionFactory>(),
|
||||||
|
TimeProvider.System);
|
||||||
|
|
||||||
|
// The real shell view model, because the thing under test is what the window hands the banner. No
|
||||||
|
// vault and no sign-in: the banner is drawn outside the unlocked half of the window on purpose.
|
||||||
|
await using var shell = new MainWindowViewModel(
|
||||||
|
new ClientPaths(directory),
|
||||||
|
caches,
|
||||||
|
workspace,
|
||||||
|
new VaultKnownHostStore(),
|
||||||
|
Substitute.For<IDeviceKeyStore>(),
|
||||||
|
(_, _) => throw new NotSupportedException("nothing here signs in"),
|
||||||
|
TimeProvider.System,
|
||||||
|
Substitute.For<ISftpSessionFactory>(),
|
||||||
|
updates: new ReadyChannel());
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await shell.Updates.CheckOnceAsync(Token);
|
||||||
|
|
||||||
|
shell.Updates.IsBannerShowing.ShouldBeTrue();
|
||||||
|
|
||||||
|
await LayoutHarness.OnTheUiThreadAsync(() => AssertTheBannerIsWiredTo(shell), Token);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (Directory.Exists(directory))
|
||||||
|
{
|
||||||
|
Directory.Delete(directory, recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AssertTheBannerIsWiredTo(MainWindowViewModel shell)
|
||||||
|
{
|
||||||
|
var window = new MainWindow { DataContext = shell };
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var banner = window.GetLogicalDescendants().OfType<UpdateBanner>().ShouldHaveSingleItem();
|
||||||
|
|
||||||
|
banner.DataContext.ShouldBeSameAs(shell.Updates);
|
||||||
|
banner.IsVisible.ShouldBeTrue();
|
||||||
|
|
||||||
|
// The defect itself: a null command is a button that answers a click by doing nothing, and it
|
||||||
|
// is indistinguishable from a working one until somebody presses it.
|
||||||
|
var restart = banner.FindControl<Button>("RestartNowButton").ShouldNotBeNull();
|
||||||
|
|
||||||
|
restart.Command.ShouldNotBeNull();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
window.Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user