using Bunit; using Microsoft.AspNetCore.Components; using ScadaLink.CentralUI.Components.Shared; namespace ScadaLink.CentralUI.Tests; /// /// bUnit tests for TreeView's Checkbox-selection mode (T19 of the Transport /// feature). Verifies that: /// - a checkbox renders next to every node in Checkbox mode, /// - clicking a folder checkbox cascades selection to every descendant leaf, /// - clicking a leaf produces a partial parent state (Indeterminate), /// - Single-mode behaviour is preserved (regression). /// public class TreeViewMultiSelectTests : BunitContext { private record TestNode(string Key, string Label, List Children); private static List TwoFoldersThreeLeaves() => new() { // Folder1 → Leaf1a, Leaf1b new("f1", "Folder1", new() { new("l1a", "Leaf1a", new()), new("l1b", "Leaf1b", new()), }), // Folder2 → Leaf2a new("f2", "Folder2", new() { new("l2a", "Leaf2a", new()), }), }; private IRenderedComponent> RenderCheckboxTree( List? items = null, HashSet? selectedKeys = null, Action>? onSelectedKeysChanged = null) { // Indeterminate is set via JS interop after each render. We stub it so // bUnit's strict mode doesn't blow up on the unmocked call. JSInterop.SetupVoid("treeviewStorage.setIndeterminate", _ => true); return Render>(parameters => { parameters .Add(p => p.Items, items ?? TwoFoldersThreeLeaves()) .Add(p => p.ChildrenSelector, n => n.Children) .Add(p => p.HasChildrenSelector, n => n.Children.Count > 0) .Add(p => p.KeySelector, n => n.Key) .Add(p => p.NodeContent, node => builder => { builder.AddMarkupContent(0, $"{node.Label}"); }) .Add(p => p.SelectionMode, TreeViewSelectionMode.Checkbox) .Add(p => p.InitiallyExpanded, _ => true) .Add(p => p.SelectedKeys, selectedKeys); if (onSelectedKeysChanged != null) { parameters.Add(p => p.SelectedKeysChanged, onSelectedKeysChanged); } }); } [Fact] public void Checkbox_mode_renders_input_checkbox_per_node() { var cut = RenderCheckboxTree(); // 2 folders + 3 leaves = 5 nodes, expanded → 5 checkboxes. var checkboxes = cut.FindAll("input.tv-checkbox"); Assert.Equal(5, checkboxes.Count); } [Fact] public void Clicking_folder_checkbox_selects_all_descendant_leaves() { HashSet? captured = null; var cut = RenderCheckboxTree( selectedKeys: new HashSet(), onSelectedKeysChanged: keys => captured = keys); // First checkbox is Folder1 (root #1). It has leaves l1a + l1b. var folderCheckbox = cut.FindAll("input.tv-checkbox")[0]; folderCheckbox.Change(true); Assert.NotNull(captured); Assert.Equal(2, captured!.Count); Assert.Contains((object)"l1a", captured); Assert.Contains((object)"l1b", captured); } [Fact] public void Clicking_leaf_makes_parent_indeterminate_when_sibling_unchecked() { // Pre-select one of Folder1's two leaves → Folder1 should compute as // Indeterminate on the next render (carries the `tv-checkbox-indeterminate` // CSS marker class — the JS-set `indeterminate` DOM property is set via // interop and isn't observable through bUnit's rendered HTML). var cut = RenderCheckboxTree( selectedKeys: new HashSet { "l1a" }); // Folder1 checkbox is the first .tv-checkbox; should carry the partial // marker class. var folderCheckbox = cut.FindAll("input.tv-checkbox")[0]; var classAttr = folderCheckbox.GetAttribute("class") ?? string.Empty; Assert.Contains("tv-checkbox-indeterminate", classAttr); // …and not the fully-checked attribute. Assert.NotEqual("true", folderCheckbox.GetAttribute("checked")); } [Fact] public void Single_mode_unchanged() { // Default SelectionMode is Single. No SelectedKeysChanged should fire, // and SelectedKeyChanged (singular) should fire on content click. object? selected = null; HashSet? bulk = null; var cut = Render>(parameters => { parameters .Add(p => p.Items, TwoFoldersThreeLeaves()) .Add(p => p.ChildrenSelector, n => n.Children) .Add(p => p.HasChildrenSelector, n => n.Children.Count > 0) .Add(p => p.KeySelector, n => n.Key) .Add(p => p.NodeContent, node => builder => { builder.AddMarkupContent(0, $"{node.Label}"); }) .Add(p => p.InitiallyExpanded, _ => true) .Add(p => p.Selectable, true) .Add(p => p.SelectedKeyChanged, (Action)(k => selected = k)) .Add(p => p.SelectedKeysChanged, (Action>)(s => bulk = s)); }); // No checkboxes rendered in Single mode. Assert.Empty(cut.FindAll("input.tv-checkbox")); // Clicking the content fires SelectedKeyChanged (singular) and NOT // SelectedKeysChanged (plural). cut.Find(".tv-content").Click(); Assert.Equal("f1", selected); Assert.Null(bulk); } }