b0959f697b
DriverConfigModal, DeviceModal and RawTagModal built a new parameter dictionary and new EventCallbacks every render. ParameterDictionaryCache reuses the dictionary while the values it carries are unchanged and rebuilds it with the current value when one changes (a build-once cache would strand edits); the callbacks are created once. Blazor still re-sets an EventCallback-bearing child's parameters per parent render, so the forms' _lastParsed guards stay. Refs #745 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
88 lines
3.3 KiB
C#
88 lines
3.3 KiB
C#
using Microsoft.AspNetCore.Components;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Raw;
|
|
|
|
/// <summary>
|
|
/// R04 H2: the modals' DynamicComponent parameter dictionaries are reused across renders while their
|
|
/// values are unchanged, and rebuilt — carrying the current value — the moment one changes. The second
|
|
/// half is the regression a naive build-once cache would introduce (stale JSON stranding every edit).
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class ParameterDictionaryCacheTests
|
|
{
|
|
[Fact]
|
|
public void Unchanged_values_return_the_same_dictionary()
|
|
{
|
|
var cache = new ParameterDictionaryCache();
|
|
var builds = 0;
|
|
var json = "{}";
|
|
Dictionary<string, object> Build() { builds++; return new() { ["Json"] = json }; }
|
|
|
|
var first = cache.Get([json], Build);
|
|
cache.Get([json], Build).ShouldBeSameAs(first);
|
|
builds.ShouldBe(1);
|
|
}
|
|
|
|
[Fact]
|
|
public void A_changed_value_rebuilds_with_the_current_value()
|
|
{
|
|
var cache = new ParameterDictionaryCache();
|
|
var json = "{}";
|
|
Dictionary<string, object> Build() => new() { ["Json"] = json };
|
|
|
|
cache.Get([json], Build)["Json"].ShouldBe("{}");
|
|
json = "{\"a\":1}";
|
|
cache.Get([json], Build)["Json"].ShouldBe("{\"a\":1}");
|
|
}
|
|
|
|
[Fact]
|
|
public void Null_values_compare_and_reset_forces_a_rebuild()
|
|
{
|
|
var cache = new ParameterDictionaryCache();
|
|
var builds = 0;
|
|
Dictionary<string, object> Build() { builds++; return new(); }
|
|
|
|
cache.Get(["x", null], Build);
|
|
cache.Get(["x", null], Build);
|
|
builds.ShouldBe(1);
|
|
|
|
cache.Reset();
|
|
cache.Get(["x", null], Build);
|
|
builds.ShouldBe(2);
|
|
}
|
|
|
|
[Fact]
|
|
public void Modals_build_their_parameters_through_the_cache_with_reused_callbacks()
|
|
{
|
|
foreach (var (path, cacheField) in new[]
|
|
{
|
|
(new[] { "Components", "Shared", "Drivers", "DriverConfigModal.razor" }, "_formParameterCache.Get("),
|
|
(new[] { "Components", "Shared", "Drivers", "DeviceModal.razor" }, "_deviceFormParameterCache.Get("),
|
|
(new[] { "Components", "Shared", "Raw", "RawTagModal.razor" }, "_editorParameterCache.Get("),
|
|
})
|
|
{
|
|
var src = AdminUiSource.Read(path);
|
|
src.ShouldContain(cacheField, customMessage: path[^1]);
|
|
// Every EventCallback is created behind a ??= so it is made once per component.
|
|
var all = System.Text.RegularExpressions.Regex.Matches(src, @"EventCallback\.Factory\.Create").Count;
|
|
var once = System.Text.RegularExpressions.Regex.Matches(src, @"\?\?=\s*EventCallback\.Factory\.Create").Count;
|
|
all.ShouldBeGreaterThan(0, path[^1]);
|
|
once.ShouldBe(all, path[^1]);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void RawTagModal_reuses_its_callback_across_renders()
|
|
{
|
|
var modal = new ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Raw.RawTagModal();
|
|
var first = modal.BuildEditorParameters();
|
|
var second = modal.BuildEditorParameters();
|
|
|
|
second.ShouldBeSameAs(first);
|
|
((EventCallback<string>)first["ConfigJsonChanged"]).HasDelegate.ShouldBeTrue();
|
|
}
|
|
}
|