Compare commits
2 commits
1733dababb
...
fdb7acca56
| Author | SHA1 | Date | |
|---|---|---|---|
| fdb7acca56 | |||
| 5104ffeb81 |
6 changed files with 152 additions and 7 deletions
125
src/PostIt.Tests/SessionStatusBannerTests.cs
Normal file
125
src/PostIt.Tests/SessionStatusBannerTests.cs
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Headless.XUnit;
|
||||||
|
using Avalonia.Media;
|
||||||
|
using Avalonia.Styling;
|
||||||
|
using Avalonia.VisualTree;
|
||||||
|
using PostIt.ViewModels;
|
||||||
|
using PostIt.Views;
|
||||||
|
|
||||||
|
namespace PostIt.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// UI tests for <see cref="SessionStatusBanner"/>. Mounted inside
|
||||||
|
/// a real <see cref="MainWindow"/> via the headless Avalonia
|
||||||
|
/// platform declared in <c>TestApp.cs</c>.
|
||||||
|
///
|
||||||
|
/// <para>The pattern is the one that <c>UnitTest1.MainPage_Should_Load</c>
|
||||||
|
/// established: a test attribute <c>[AvaloniaFact]</c> (from
|
||||||
|
/// <c>Avalonia.Headless.XUnit</c>) instead of plain <c>[Fact]</c>,
|
||||||
|
/// <c>new MainWindow()</c>, <c>window.Show()</c>. The AvaloniaFact
|
||||||
|
/// attribute schedules the test body inside a dispatcher, which
|
||||||
|
/// is the precondition for the headless Window's
|
||||||
|
/// <c>PlatformManager.CreateWindow()</c> to find a registered
|
||||||
|
/// service. A plain <c>[Fact]</c> test that calls
|
||||||
|
/// <c>new Window().Show()</c> throws because the harness has not
|
||||||
|
/// been initialised for that thread.</para>
|
||||||
|
///
|
||||||
|
/// <para>The session banner's <c>DataContext</c> is not wired in
|
||||||
|
/// these tests: <c>App.OnFrameworkInitializationCompleted</c> is
|
||||||
|
/// not called in a unit test, so we set the DataContext on the
|
||||||
|
/// banner directly. The production code path is exercised
|
||||||
|
/// end-to-end by the manual launch, not here.</para>
|
||||||
|
/// </summary>
|
||||||
|
public class SessionStatusBannerTests
|
||||||
|
{
|
||||||
|
[AvaloniaFact]
|
||||||
|
public void Banner_renders_three_buttons_in_the_visual_tree()
|
||||||
|
{
|
||||||
|
var window = new MainWindow();
|
||||||
|
window.SessionBanner.DataContext = new SessionStatusViewModel();
|
||||||
|
window.Show();
|
||||||
|
|
||||||
|
var buttons = window.SessionBanner.GetVisualDescendants()
|
||||||
|
.OfType<Button>()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// Three buttons, named by their content text: Se
|
||||||
|
// déconnecter, Se connecter, Paramètres. If any one is
|
||||||
|
// missing, the user has no way to trigger the
|
||||||
|
// corresponding navigation event.
|
||||||
|
Assert.Equal(3, buttons.Count);
|
||||||
|
Assert.Contains(buttons, b => b.Content as string == "Se déconnecter");
|
||||||
|
Assert.Contains(buttons, b => b.Content as string == "Se connecter");
|
||||||
|
Assert.Contains(buttons, b => b.Content as string == "Paramètres");
|
||||||
|
}
|
||||||
|
|
||||||
|
[AvaloniaFact]
|
||||||
|
public void Banner_login_button_is_visible_when_logged_out()
|
||||||
|
{
|
||||||
|
var window = new MainWindow();
|
||||||
|
var vm = new SessionStatusViewModel();
|
||||||
|
Assert.True(vm.IsLoggedOut); // VM default
|
||||||
|
window.SessionBanner.DataContext = vm;
|
||||||
|
window.Show();
|
||||||
|
|
||||||
|
var login = window.SessionBanner.GetVisualDescendants()
|
||||||
|
.OfType<Button>()
|
||||||
|
.Single(b => b.Content as string == "Se connecter");
|
||||||
|
|
||||||
|
// The XAML binds IsVisible to IsLoggedOut. After Show,
|
||||||
|
// the binding has been evaluated.
|
||||||
|
Assert.True(login.IsVisible);
|
||||||
|
}
|
||||||
|
|
||||||
|
[AvaloniaFact]
|
||||||
|
public void Banner_logout_button_is_hidden_when_logged_out()
|
||||||
|
{
|
||||||
|
var window = new MainWindow();
|
||||||
|
var vm = new SessionStatusViewModel();
|
||||||
|
Assert.False(vm.IsLoggedIn); // VM default
|
||||||
|
window.SessionBanner.DataContext = vm;
|
||||||
|
window.Show();
|
||||||
|
|
||||||
|
var logout = window.SessionBanner.GetVisualDescendants()
|
||||||
|
.OfType<Button>()
|
||||||
|
.Single(b => b.Content as string == "Se déconnecter");
|
||||||
|
|
||||||
|
Assert.False(logout.IsVisible);
|
||||||
|
}
|
||||||
|
|
||||||
|
[AvaloniaFact]
|
||||||
|
public void Banner_settings_button_is_visible_regardless_of_session()
|
||||||
|
{
|
||||||
|
var window = new MainWindow();
|
||||||
|
window.SessionBanner.DataContext = new SessionStatusViewModel();
|
||||||
|
window.Show();
|
||||||
|
|
||||||
|
var settings = window.SessionBanner.GetVisualDescendants()
|
||||||
|
.OfType<Button>()
|
||||||
|
.Single(b => b.Content as string == "Paramètres");
|
||||||
|
|
||||||
|
// Paramètres is the only button with no IsVisible
|
||||||
|
// binding — always shown. The user's only path to the
|
||||||
|
// settings page goes through this button.
|
||||||
|
Assert.True(settings.IsVisible);
|
||||||
|
}
|
||||||
|
|
||||||
|
[AvaloniaFact]
|
||||||
|
public void Banner_session_label_reflects_DataContext()
|
||||||
|
{
|
||||||
|
var window = new MainWindow();
|
||||||
|
window.SessionBanner.DataContext = new SessionStatusViewModel();
|
||||||
|
window.Show();
|
||||||
|
|
||||||
|
var label = window.SessionBanner.GetVisualDescendants()
|
||||||
|
.OfType<TextBlock>()
|
||||||
|
.First(t => t.Text == "Déconnecté" || t.Text == "Connecté");
|
||||||
|
|
||||||
|
// Default SessionLabel is "Déconnecté" until Refresh()
|
||||||
|
// is called with a valid session. This pins the default
|
||||||
|
// so a future refactor that breaks the initial value
|
||||||
|
// (e.g. by removing the field initialiser) is caught.
|
||||||
|
Assert.Equal("Déconnecté", label.Text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
<PackageVersion Include="Avalonia.Themes.Fluent" Version="12.0.4" />
|
<PackageVersion Include="Avalonia.Themes.Fluent" Version="12.0.4" />
|
||||||
<PackageVersion Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.2" />
|
<PackageVersion Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.2" />
|
||||||
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.2" />
|
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.2" />
|
||||||
|
<PackageVersion Include="Material.Avalonia" Version="3.17.0" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.9" />
|
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.9" />
|
||||||
<PackageVersion Include="Xamarin.AndroidX.Browser" Version="1.8.0" />
|
<PackageVersion Include="Xamarin.AndroidX.Browser" Version="1.8.0" />
|
||||||
<PackageVersion Include="Xamarin.AndroidX.Core.SplashScreen" Version="1.2.0" />
|
<PackageVersion Include="Xamarin.AndroidX.Core.SplashScreen" Version="1.2.0" />
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@
|
||||||
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
|
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
|
||||||
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
|
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
<PackageReference Include="Material.Avalonia" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\PostIt\PostIt.csproj" />
|
<ProjectReference Include="..\PostIt\PostIt.csproj" />
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Controls.ApplicationLifetimes;
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
|
using Avalonia.Styling;
|
||||||
using PostIt.Services;
|
using PostIt.Services;
|
||||||
using PostIt.ViewModels;
|
using PostIt.ViewModels;
|
||||||
using PostIt.Views;
|
using PostIt.Views;
|
||||||
|
|
@ -112,8 +113,24 @@ public partial class App : Application
|
||||||
// DataContext, and the TwoWay bindings inside the page keep
|
// DataContext, and the TwoWay bindings inside the page keep
|
||||||
// mutating the same in-memory Settings instance that the rest
|
// mutating the same in-memory Settings instance that the rest
|
||||||
// of the app reads (OidcClientOptions construction, etc.).
|
// of the app reads (OidcClientOptions construction, etc.).
|
||||||
provider.GetRequiredService<SettingsPage>().DataContext =
|
provider.GetRequiredService<SettingsPage>().DataContext = settings;
|
||||||
provider.GetRequiredService<Settings>();
|
|
||||||
|
// Settings.DarkMode was previously a dead field: it round-
|
||||||
|
// tripped through the settings file and the SettingsPage
|
||||||
|
// CheckBox, but no consumer ever read it. Wire it here to
|
||||||
|
// Application.RequestedThemeVariant so the toggle takes
|
||||||
|
// effect immediately, and seed the initial theme from the
|
||||||
|
// value Load() just populated (so a dark-mode user lands on
|
||||||
|
// a dark window on first launch, not on a default-light
|
||||||
|
// window that flips after the user touches the toggle).
|
||||||
|
ApplyDarkMode(settings);
|
||||||
|
settings.PropertyChanged += (_, e) =>
|
||||||
|
{
|
||||||
|
if (e.PropertyName == nameof(Settings.DarkMode))
|
||||||
|
{
|
||||||
|
ApplyDarkMode(settings);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
{
|
{
|
||||||
|
|
@ -190,6 +207,12 @@ public partial class App : Application
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void ApplyDarkMode(Settings settings)
|
||||||
|
{
|
||||||
|
Application.Current!.RequestedThemeVariant =
|
||||||
|
settings.DarkMode ? ThemeVariant.Dark : ThemeVariant.Light;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Run once after the main window is shown: try to refresh the
|
/// Run once after the main window is shown: try to refresh the
|
||||||
/// cached OIDC tokens silently; on success, push MainPage on top
|
/// cached OIDC tokens silently; on success, push MainPage on top
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ using System;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Avalonia.Styling;
|
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using PostIt.Models;
|
using PostIt.Models;
|
||||||
|
|
@ -38,9 +37,6 @@ public partial class MainPageViewModel : ViewModelBase
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial bool IsBusy { get; set; }
|
public partial bool IsBusy { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
ThemeVariant themeVariant = ThemeVariant.Default;
|
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Settings Settings { get; private set; }
|
public partial Settings Settings { get; private set; }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
x:Class="PostIt.Views.SessionStatusBanner"
|
x:Class="PostIt.Views.SessionStatusBanner"
|
||||||
x:DataType="vm:SessionStatusViewModel">
|
x:DataType="vm:SessionStatusViewModel">
|
||||||
<Border DockPanel.Dock="Top"
|
<Border DockPanel.Dock="Top"
|
||||||
Background="#ECEFF1"
|
|
||||||
BorderBrush="#B0BEC5"
|
BorderBrush="#B0BEC5"
|
||||||
BorderThickness="0,0,0,1"
|
BorderThickness="0,0,0,1"
|
||||||
Padding="12,6">
|
Padding="12,6">
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue