feat: WinPE pre-config collector + simplified first-boot toolbox (SP1) #21
@@ -60,7 +60,7 @@ else
|
|||||||
<PrefsStep />
|
<PrefsStep />
|
||||||
break;
|
break;
|
||||||
case 4:
|
case 4:
|
||||||
<ApplyStep OnComplete="AdvanceToDone" OnRunningChanged="@(v => { _applyRunning = v; StateHasChanged(); })" />
|
<ApplyStep AutoStart="_autoApply" OnComplete="AdvanceToDone" OnRunningChanged="@(v => { _applyRunning = v; StateHasChanged(); })" />
|
||||||
break;
|
break;
|
||||||
case 5:
|
case 5:
|
||||||
<DoneStep />
|
<DoneStep />
|
||||||
@@ -104,6 +104,7 @@ else
|
|||||||
private bool _loading = true;
|
private bool _loading = true;
|
||||||
private bool _applyRunning = false;
|
private bool _applyRunning = false;
|
||||||
private bool _toolboxHome = false;
|
private bool _toolboxHome = false;
|
||||||
|
private bool _autoApply = false;
|
||||||
private string? _error;
|
private string? _error;
|
||||||
private IReadOnlyList<FlavourManifest> _flavours = Array.Empty<FlavourManifest>();
|
private IReadOnlyList<FlavourManifest> _flavours = Array.Empty<FlavourManifest>();
|
||||||
|
|
||||||
@@ -167,6 +168,12 @@ else
|
|||||||
|
|
||||||
if (pre.Bitlocker.Enable && !string.IsNullOrEmpty(pre.Bitlocker.Pin))
|
if (pre.Bitlocker.Enable && !string.IsNullOrEmpty(pre.Bitlocker.Pin))
|
||||||
State.BitLockerPin = pre.Bitlocker.Pin;
|
State.BitLockerPin = pre.Bitlocker.Pin;
|
||||||
|
|
||||||
|
// First run with a preconfig: skip the manual walkthrough. Jump straight to the
|
||||||
|
// Apply step and signal it to auto-start (spec §4d: auto-runs once, shows progress
|
||||||
|
// + recovery key, then Done).
|
||||||
|
_currentStep = 4;
|
||||||
|
_autoApply = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ReRunSetup()
|
private void ReRunSetup()
|
||||||
|
|||||||
@@ -50,8 +50,10 @@
|
|||||||
@code {
|
@code {
|
||||||
[Parameter] public EventCallback OnComplete { get; set; }
|
[Parameter] public EventCallback OnComplete { get; set; }
|
||||||
[Parameter] public EventCallback<bool> OnRunningChanged { get; set; }
|
[Parameter] public EventCallback<bool> OnRunningChanged { get; set; }
|
||||||
|
[Parameter] public bool AutoStart { get; set; }
|
||||||
|
|
||||||
private bool _running;
|
private bool _running;
|
||||||
|
private bool _autoStarted;
|
||||||
private bool _complete;
|
private bool _complete;
|
||||||
private int _percent;
|
private int _percent;
|
||||||
private string _stageLabel = "Preparing…";
|
private string _stageLabel = "Preparing…";
|
||||||
@@ -71,6 +73,18 @@
|
|||||||
: single[..ErrorDisplayMaxLength] + "…";
|
: single[..ErrorDisplayMaxLength] + "…";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
// First-run auto-apply: when the host jumps straight to this step with AutoStart,
|
||||||
|
// kick off the same apply the Start button would, exactly once. The manual path
|
||||||
|
// (AutoStart=false) is untouched.
|
||||||
|
if (firstRender && AutoStart && !_autoStarted)
|
||||||
|
{
|
||||||
|
_autoStarted = true;
|
||||||
|
await StartAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task StartAsync()
|
public async Task StartAsync()
|
||||||
{
|
{
|
||||||
// Re-entrancy guard: prevent a second overlapping apply if already running
|
// Re-entrancy guard: prevent a second overlapping apply if already running
|
||||||
|
|||||||
@@ -73,6 +73,33 @@ public class ApplyStepTests : TestContext
|
|||||||
Assert.True(completed);
|
Assert.True(completed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AutoStart_triggers_apply_once_without_a_button_click()
|
||||||
|
{
|
||||||
|
var apply = new Mock<IApplyService>();
|
||||||
|
apply.Setup(a => a.RunAsync(It.IsAny<ApplyRequest>(), It.IsAny<IProgress<ApplyProgress>>(), It.IsAny<CancellationToken>()))
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
var state = new WizardState
|
||||||
|
{
|
||||||
|
Flavour = new FlavourManifest { Id = "daily-driver", Hardening = new() { Modules = new[] { "00" } } },
|
||||||
|
BitLockerPin = "123456"
|
||||||
|
};
|
||||||
|
Services.AddSingleton(state);
|
||||||
|
Services.AddSingleton(apply.Object);
|
||||||
|
AddCatalog(Services);
|
||||||
|
AddPreconfig(Services);
|
||||||
|
|
||||||
|
// AutoStart=true should fire StartAsync from OnAfterRenderAsync on first render,
|
||||||
|
// with no Start button click.
|
||||||
|
var cut = RenderComponent<ApplyStep>(p => p.Add(s => s.AutoStart, true));
|
||||||
|
|
||||||
|
cut.WaitForAssertion(() =>
|
||||||
|
apply.Verify(a => a.RunAsync(
|
||||||
|
It.Is<ApplyRequest>(r => r.Flavour.Id == "daily-driver"),
|
||||||
|
It.IsAny<IProgress<ApplyProgress>>(),
|
||||||
|
It.IsAny<CancellationToken>()), Times.Once));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Shows_error_and_retry_button_when_apply_fails()
|
public async Task Shows_error_and_retry_button_when_apply_fails()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user