fix(adminui,client): error boundary inside the interactive scope, stale-commit OnSaved, AbCip browse delay emit, DisposeBridge cancel (review R3)
M1: PageErrorBoundary wraps the body of every InteractiveServer page; the inert layout boundary is removed. m1: DisposeBridge cancels its CTS when the 5 s wait times out. m2: a CSV import that commits after the modal closed still raises OnSaved. m3: document SecurityMode=None no-fallback in Client.CLI.md / Client.UI.md. m4: browsePageDelayMs written only when loaded or changed; negative flagged. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
@@ -57,6 +57,50 @@ public sealed class ModalStaleOpenGuardTests : IDisposable
|
||||
Field<string>(modal, "_effectiveDeviceId").ShouldBe("");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CsvImport_commit_that_lands_after_the_modal_closed_still_raises_OnSaved()
|
||||
{
|
||||
// A write that has landed must refresh the parent's tree even when the operator closed the modal
|
||||
// mid-commit; before the fix the stale guard returned ahead of OnSaved.
|
||||
var saved = await CommitWhileClosed(new RawTagImportResult(1, []));
|
||||
saved.ShouldBe(1);
|
||||
|
||||
// Control: a failed (all-or-nothing) import wrote nothing, so there is nothing to refresh.
|
||||
var savedOnFailure = await CommitWhileClosed(new RawTagImportResult(0, ["row 1: nope"]));
|
||||
savedOnFailure.ShouldBe(0);
|
||||
}
|
||||
|
||||
private async Task<int> CommitWhileClosed(RawTagImportResult outcome)
|
||||
{
|
||||
var gate = new TaskCompletionSource();
|
||||
var svc = Stub<IRawTreeService>(async (method, _) =>
|
||||
{
|
||||
if (method.Name != nameof(IRawTreeService.ImportTagsAsync)) { return null; }
|
||||
await gate.Task;
|
||||
return outcome;
|
||||
});
|
||||
var modal = Make<RawCsvImportModal>(("Svc", svc));
|
||||
var saved = 0;
|
||||
SetParams(modal,
|
||||
("Visible", true), ("DeviceId", "dev-A"), ("TagGroupId", null),
|
||||
("DriverType", "Modbus"),
|
||||
("OnSaved", EventCallback.Factory.Create(this, () => { saved++; })));
|
||||
await Open(modal);
|
||||
|
||||
modal.GetType().GetField("_result", BindingFlags.NonPublic | BindingFlags.Instance)!.SetValue(
|
||||
modal, RawTagCsvMapper.Parse("Modbus", "Name,DataType,TagConfigJson\nok,Float,\"{\"\"region\"\":\"\"HoldingRegisters\"\",\"\"address\"\":1}\""));
|
||||
|
||||
var commit = (Task)modal.GetType().GetMethod("Commit", BindingFlags.NonPublic | BindingFlags.Instance)!
|
||||
.Invoke(modal, null)!;
|
||||
|
||||
SetParams(modal, ("Visible", false));
|
||||
await Open(modal);
|
||||
|
||||
gate.SetResult();
|
||||
await commit;
|
||||
return saved;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CsvExport_stale_group_resolve_never_retargets_the_live_open()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user