using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared; using ZB.MOM.WW.OtOpcUa.AdminUI.Uns; namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Raw; /// R04 P5: the Raw tree renders a flat, paged row list through Virtualize, with cached menus. [Trait("Category", "Unit")] public sealed class RawTreeRowsTests { private static RawNode Node(string key, RawNodeKind kind = RawNodeKind.Tag, bool lazy = false, bool enabled = true) => new() { Kind = kind, Key = key, DisplayName = key, HasLazyChildren = lazy, Enabled = enabled }; private static RawNode Device(string key, int tags) { var d = Node(key, RawNodeKind.Device, lazy: true); for (var i = 0; i < tags; i++) d.Children.Add(Node($"{key}/t{i}")); d.Loaded = true; d.Expanded = true; return d; } [Fact] public void Collapsed_nodes_show_no_children() { var dev = Device("dev", 3); dev.Expanded = false; RawTreeRows.Flatten([dev]).Select(r => r.Key).ShouldBe(["dev"]); } [Fact] public void Expanded_nodes_flatten_depth_first_in_display_order_with_depth() { var root = Node("cluster", RawNodeKind.Cluster, lazy: true); root.Expanded = true; var drv = Node("drv", RawNodeKind.Driver, lazy: true); drv.Expanded = true; drv.Children.Add(Device("devA", 2)); root.Children.Add(drv); root.Children.Add(Node("folderB", RawNodeKind.Folder, lazy: true)); var rows = RawTreeRows.Flatten([root]); rows.Select(r => (r.Key, r.Depth)).ShouldBe( [ ("cluster", 0), ("drv", 1), ("devA", 2), ("devA/t0", 3), ("devA/t1", 3), ("folderB", 1), ]); } [Fact] public void A_5k_tag_device_shows_one_page_then_a_show_more_row() { var dev = Device("dev", 5000); var rows = RawTreeRows.Flatten([dev]); rows.Count.ShouldBe(1 + RawTreeRows.DefaultPageSize + 1); var more = rows[^1]; more.Kind.ShouldBe(RawTreeRowKind.ShowMore); more.Remaining.ShouldBe(5000 - RawTreeRows.DefaultPageSize); more.Depth.ShouldBe(1); RawTreeRows.ShowMore(dev); var after = RawTreeRows.Flatten([dev]); after.Count(r => r.Kind == RawTreeRowKind.Node).ShouldBe(1 + 2 * RawTreeRows.DefaultPageSize); after[^1].Remaining.ShouldBe(5000 - 2 * RawTreeRows.DefaultPageSize); } [Fact] public void A_level_at_or_under_the_page_size_has_no_show_more_row() { var rows = RawTreeRows.Flatten([Device("dev", RawTreeRows.DefaultPageSize)]); rows.ShouldNotContain(r => r.Kind == RawTreeRowKind.ShowMore); // Positive control: one more child does produce one. RawTreeRows.Flatten([Device("dev", RawTreeRows.DefaultPageSize + 1)]) .Count(r => r.Kind == RawTreeRowKind.ShowMore).ShouldBe(1); } [Fact] public void Show_more_row_follows_the_whole_subtree_of_the_last_shown_child() { var dev = Node("dev", RawNodeKind.Device, lazy: true); dev.Expanded = true; dev.VisibleChildLimit = 1; var grp = Node("grp", RawNodeKind.TagGroup, lazy: true); grp.Expanded = true; grp.Children.Add(Node("grp/t")); dev.Children.Add(grp); dev.Children.Add(Node("dev/hidden")); RawTreeRows.Flatten([dev]).Select(r => r.Key).ShouldBe(["dev", "grp", "grp/t", "more:dev"]); } [Fact] public void Loading_and_error_rows_replace_children() { var loading = Device("a", 2); loading.Loading = true; var failed = Device("b", 2); failed.Error = "boom"; RawTreeRows.Flatten([loading, failed]).Select(r => r.Kind).ShouldBe( [ RawTreeRowKind.Node, RawTreeRowKind.Loading, RawTreeRowKind.Node, RawTreeRowKind.Error, ]); } [Fact] public void Menu_cache_reuses_the_list_and_rebuilds_when_a_dependency_changes() { var cache = new RawNodeMenuCache(); var builds = 0; IReadOnlyList Build(RawNode _) { builds++; return [new ContextMenuItem { Label = "x" }]; } var node = Node("drv", RawNodeKind.Driver); var first = cache.GetOrBuild(node, pickerMode: false, Build); cache.GetOrBuild(node, pickerMode: false, Build).ShouldBeSameAs(first); builds.ShouldBe(1); // Picker mode is a different menu entirely. cache.GetOrBuild(node, pickerMode: true, Build).ShouldNotBeSameAs(first); builds.ShouldBe(2); // A reload replaces the node instance (Enabled is init-only), so the new node gets its own menu. var reloaded = Node("drv", RawNodeKind.Driver, enabled: false); cache.GetOrBuild(reloaded, pickerMode: true, Build); builds.ShouldBe(3); } [Fact] public void RawTree_renders_through_Virtualize_and_the_menu_cache() { var src = AdminUiSource.Read("Components", "Shared", "Raw", "RawTree.razor"); src.ShouldContain("