fix(welcome): notify wizard host on AccountStep validity change so Next enables (live e2e blocker) + regression test
All checks were successful
Build SilverMetal Enhanced - Windows ISO / build (pull_request) Successful in 4m35s
All checks were successful
Build SilverMetal Enhanced - Windows ISO / build (pull_request) Successful in 4m35s
AccountStep now exposes OnValidityChanged EventCallback<bool> and fires it at the end of every Validate() call (including OnInitialized). Routes.razor drops the @ref/IsValid polling pattern in favour of _accountValid updated via the callback + StateHasChanged, matching the existing OnRunningChanged pattern used by ApplyStep. Adds 5 bUnit regression tests covering: initial-invalid, all-valid, re-invalid on clear, short/non-numeric PIN, and pre-populated state on Back→Forward re-mount. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -40,7 +40,7 @@
|
||||
<FlavourStep Flavours="_flavours" />
|
||||
break;
|
||||
case 2:
|
||||
<AccountStep @ref="_accountStep" />
|
||||
<AccountStep OnValidityChanged="@(v => { _accountValid = v; StateHasChanged(); })" />
|
||||
break;
|
||||
case 3:
|
||||
<PrefsStep />
|
||||
@@ -82,14 +82,14 @@
|
||||
private int _currentStep = 0;
|
||||
private bool _loading = true;
|
||||
private bool _applyRunning = false;
|
||||
private bool _accountValid = false;
|
||||
private string? _error;
|
||||
private IReadOnlyList<FlavourManifest> _flavours = Array.Empty<FlavourManifest>();
|
||||
private AccountStep? _accountStep;
|
||||
|
||||
private bool CanGoNext => _currentStep switch
|
||||
{
|
||||
1 => State.Flavour is not null,
|
||||
2 => _accountStep?.IsValid ?? false,
|
||||
2 => _accountValid,
|
||||
_ => true
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
@inject WizardState State
|
||||
|
||||
<div class="step account-step">
|
||||
|
||||
<h1>Set Up Your Account</h1>
|
||||
<p class="step-subtitle">Create your daily-use account and administrator credentials.</p>
|
||||
|
||||
@@ -53,7 +54,10 @@
|
||||
private readonly Dictionary<string, string> _errors = new();
|
||||
private readonly HashSet<string> _touched = new();
|
||||
|
||||
/// <summary>True when all fields are valid. Used by the wizard host to gate Next.</summary>
|
||||
/// <summary>Notifies the wizard host whenever validity changes (and on initial mount).</summary>
|
||||
[Parameter] public EventCallback<bool> OnValidityChanged { get; set; }
|
||||
|
||||
/// <summary>True when all fields are valid.</summary>
|
||||
public bool IsValid { get; private set; }
|
||||
|
||||
protected override void OnInitialized() => Validate();
|
||||
@@ -81,5 +85,6 @@
|
||||
_errors["bitlockerpin"] = "BitLocker PIN must be all digits and at least 6 digits long.";
|
||||
|
||||
IsValid = _errors.Count == 0;
|
||||
_ = OnValidityChanged.InvokeAsync(IsValid);
|
||||
}
|
||||
}
|
||||
|
||||
103
windows/welcome/tests/SilverOS.Welcome.Tests/AccountStepTests.cs
Normal file
103
windows/welcome/tests/SilverOS.Welcome.Tests/AccountStepTests.cs
Normal file
@@ -0,0 +1,103 @@
|
||||
using Bunit;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using SilverOS.Welcome.App.Components;
|
||||
using SilverOS.Welcome.App.Components.Steps;
|
||||
using Xunit;
|
||||
|
||||
public class AccountStepTests : TestContext
|
||||
{
|
||||
// Helper: register WizardState and render AccountStep with an OnValidityChanged capture.
|
||||
private (IRenderedComponent<AccountStep> cut, Func<bool?> lastValidity) RenderStep(WizardState? state = null)
|
||||
{
|
||||
var wizardState = state ?? new WizardState();
|
||||
Services.AddSingleton(wizardState);
|
||||
|
||||
bool? captured = null;
|
||||
var cut = RenderComponent<AccountStep>(p =>
|
||||
p.Add(s => s.OnValidityChanged,
|
||||
EventCallback.Factory.Create<bool>(this, v => captured = v)));
|
||||
|
||||
return (cut, () => captured);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnValidityChanged_fires_false_on_initial_mount_with_empty_fields()
|
||||
{
|
||||
var (_, lastValidity) = RenderStep();
|
||||
|
||||
Assert.NotNull(lastValidity());
|
||||
Assert.False(lastValidity(), "Step should be invalid on first mount (empty fields).");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnValidityChanged_fires_true_after_all_valid_inputs_are_entered()
|
||||
{
|
||||
var (cut, lastValidity) = RenderStep();
|
||||
|
||||
// Simulate user filling in all four fields.
|
||||
cut.Find("#username").Input("alice");
|
||||
cut.Find("#password").Input("Secret1!");
|
||||
cut.Find("#adminpassword").Input("Admin1!");
|
||||
cut.Find("#bitlockerpin").Input("123456");
|
||||
|
||||
Assert.True(lastValidity(), "Step should be valid after all fields are correctly filled.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnValidityChanged_fires_false_when_a_field_is_cleared_after_being_valid()
|
||||
{
|
||||
var (cut, lastValidity) = RenderStep();
|
||||
|
||||
cut.Find("#username").Input("alice");
|
||||
cut.Find("#password").Input("Secret1!");
|
||||
cut.Find("#adminpassword").Input("Admin1!");
|
||||
cut.Find("#bitlockerpin").Input("123456");
|
||||
|
||||
Assert.True(lastValidity()); // sanity
|
||||
|
||||
// Clear a required field — must revert to invalid.
|
||||
cut.Find("#username").Input("");
|
||||
|
||||
Assert.False(lastValidity(), "Step should become invalid again when a required field is cleared.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnValidityChanged_fires_false_when_pin_is_non_numeric_or_too_short()
|
||||
{
|
||||
var (cut, lastValidity) = RenderStep();
|
||||
|
||||
cut.Find("#username").Input("alice");
|
||||
cut.Find("#password").Input("Secret1!");
|
||||
cut.Find("#adminpassword").Input("Admin1!");
|
||||
|
||||
// Too short — 5 digits.
|
||||
cut.Find("#bitlockerpin").Input("12345");
|
||||
Assert.False(lastValidity(), "PIN with only 5 digits must be invalid.");
|
||||
|
||||
// Non-numeric.
|
||||
cut.Find("#bitlockerpin").Input("abc123");
|
||||
Assert.False(lastValidity(), "Non-numeric PIN must be invalid.");
|
||||
|
||||
// Exactly 6 digits — valid.
|
||||
cut.Find("#bitlockerpin").Input("123456");
|
||||
Assert.True(lastValidity(), "Exactly 6 numeric digits is valid.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnValidityChanged_fires_true_on_mount_when_wizard_state_already_populated()
|
||||
{
|
||||
var prefilledState = new WizardState
|
||||
{
|
||||
Username = "alice",
|
||||
Password = "Secret1!",
|
||||
AdminPassword = "Admin1!",
|
||||
BitLockerPin = "123456"
|
||||
};
|
||||
|
||||
var (_, lastValidity) = RenderStep(prefilledState);
|
||||
|
||||
Assert.True(lastValidity(),
|
||||
"Step should fire valid=true on mount when WizardState already has valid values (Back→Forward re-mount).");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user