Files
radzen-blazor/Radzen.Blazor.Tests/TabsTests.cs
Vladimir Enchev 61b2982f0d RadzenTabs: ensure render state is restored when tab event handlers throw
Follow-up to the #2558 fix. In SelectTab the StateHasChanged call was skipped
if Change/SelectedIndexChanged threw, leaving the component stale (multiple
tabs visually active); in SelectTabOnClient a throw left shouldRender stuck at
false, blocking all further renders. Wrap both in finally so the render state
is always restored. Addresses review feedback on commit df1dfa713.

Fixes #2558
2026-05-29 15:19:30 +03:00

280 lines
9.8 KiB
C#

using Bunit;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using Xunit;
namespace Radzen.Blazor.Tests
{
public class TabsTests
{
[Fact]
public void Tabs_Renders_WithClassName()
{
using var ctx = new TestContext();
var component = ctx.RenderComponent<RadzenTabs>();
Assert.Contains(@"rz-tabview", component.Markup);
}
[Fact]
public void Tabs_Renders_TabPosition_Top()
{
using var ctx = new TestContext();
var component = ctx.RenderComponent<RadzenTabs>(parameters =>
{
parameters.Add(p => p.TabPosition, TabPosition.Top);
});
Assert.Contains("rz-tabview-top", component.Markup);
}
[Fact]
public void Tabs_Renders_TabPosition_Bottom()
{
using var ctx = new TestContext();
var component = ctx.RenderComponent<RadzenTabs>(parameters =>
{
parameters.Add(p => p.TabPosition, TabPosition.Bottom);
});
Assert.Contains("rz-tabview-bottom", component.Markup);
}
[Fact]
public void Tabs_Renders_TabPosition_Left()
{
using var ctx = new TestContext();
var component = ctx.RenderComponent<RadzenTabs>(parameters =>
{
parameters.Add(p => p.TabPosition, TabPosition.Left);
});
Assert.Contains("rz-tabview-left", component.Markup);
}
[Fact]
public void Tabs_Renders_TabPosition_Right()
{
using var ctx = new TestContext();
var component = ctx.RenderComponent<RadzenTabs>(parameters =>
{
parameters.Add(p => p.TabPosition, TabPosition.Right);
});
Assert.Contains("rz-tabview-right", component.Markup);
}
[Fact]
public void Tabs_Renders_TabPosition_TopRight()
{
using var ctx = new TestContext();
var component = ctx.RenderComponent<RadzenTabs>(parameters =>
{
parameters.Add(p => p.TabPosition, TabPosition.TopRight);
});
Assert.Contains("rz-tabview-top-right", component.Markup);
}
[Fact]
public void Tabs_Renders_TabPosition_BottomRight()
{
using var ctx = new TestContext();
var component = ctx.RenderComponent<RadzenTabs>(parameters =>
{
parameters.Add(p => p.TabPosition, TabPosition.BottomRight);
});
Assert.Contains("rz-tabview-bottom-right", component.Markup);
}
[Fact]
public void Tabs_Renders_TabNav()
{
using var ctx = new TestContext();
var component = ctx.RenderComponent<RadzenTabs>();
Assert.Contains("rz-tabview-nav", component.Markup);
}
[Fact]
public void Tabs_Renders_TabPanels()
{
using var ctx = new TestContext();
var component = ctx.RenderComponent<RadzenTabs>();
Assert.Contains("rz-tabview-panels", component.Markup);
}
[Fact]
public void Tabs_Wrapper_IsFocusTarget_WithRoleTablist()
{
using var ctx = new TestContext();
var component = ctx.RenderComponent<RadzenTabs>();
var wrapper = component.Find("div.rz-tabview");
Assert.Equal("tablist", wrapper.GetAttribute("role"));
Assert.Equal("0", wrapper.GetAttribute("tabindex"));
}
[Fact]
public void Tabs_InnerNav_HasRolePresentation_ToAvoidDuplicateTablist()
{
using var ctx = new TestContext();
var component = ctx.RenderComponent<RadzenTabs>();
var nav = component.Find("ul.rz-tabview-nav");
Assert.NotEqual("tablist", nav.GetAttribute("role"));
}
static RenderFragment TabsFragment(params string[] titles) => builder =>
{
for (var i = 0; i < titles.Length; i++)
{
builder.OpenComponent(i, typeof(RadzenTabsItem));
builder.AddAttribute(i + 1, "Text", titles[i]);
builder.CloseComponent();
}
};
[Fact]
public void Tabs_Wrapper_HasAriaActiveDescendant_PointingAt_SelectedTab()
{
using var ctx = new TestContext();
var component = ctx.RenderComponent<RadzenTabs>(parameters => parameters
.Add(p => p.SelectedIndex, 1)
.Add(p => p.Tabs, TabsFragment("One", "Two"))
);
var wrapper = component.Find("div.rz-tabview");
var active = wrapper.GetAttribute("aria-activedescendant");
Assert.False(string.IsNullOrEmpty(active));
var activeTab = component.Find($"[id='{active}']");
Assert.Equal("tab", activeTab.GetAttribute("role"));
Assert.Equal("true", activeTab.GetAttribute("aria-selected"));
}
[Fact]
public void Tabs_OnlyOneTab_HasAriaSelectedTrue()
{
using var ctx = new TestContext();
var component = ctx.RenderComponent<RadzenTabs>(parameters => parameters
.Add(p => p.SelectedIndex, 2)
.Add(p => p.Tabs, TabsFragment("One", "Two", "Three"))
);
var selectedTabs = component.FindAll("[role='tab'][aria-selected='true']");
Assert.Single(selectedTabs);
}
static RenderFragment TabsFragmentWithContent(params (string Title, string Content)[] tabs) => builder =>
{
var seq = 0;
foreach (var (title, content) in tabs)
{
builder.OpenComponent(seq++, typeof(RadzenTabsItem));
builder.AddAttribute(seq++, "Text", title);
builder.AddAttribute(seq++, "ChildContent", (RenderFragment)(b => b.AddContent(0, content)));
builder.CloseComponent();
}
};
[Fact]
public void Tabs_KeyDownInPanelContent_DoesNotReRenderContent()
{
using var ctx = new TestContext();
var contentRenderCount = 0;
RenderFragment content = b => { contentRenderCount++; b.AddContent(0, "Panel-Content"); };
RenderFragment tabs = builder =>
{
builder.OpenComponent(0, typeof(RadzenTabsItem));
builder.AddAttribute(1, "Text", "First");
builder.AddAttribute(2, "ChildContent", content);
builder.CloseComponent();
};
var component = ctx.RenderComponent<RadzenTabs>(parameters => parameters
.Add(p => p.SelectedIndex, 0)
.Add(p => p.Tabs, tabs)
);
Assert.Contains("Panel-Content", component.Markup);
var renderCountBefore = contentRenderCount;
var panel = component.Find("div.rz-tabview-panel");
panel.KeyDown(new KeyboardEventArgs { Key = "a", Code = "KeyA" });
panel.KeyDown(new KeyboardEventArgs { Key = "b", Code = "KeyB" });
Assert.Equal(renderCountBefore, contentRenderCount);
}
[Fact]
public void Tabs_EscapeKeyDownInPanelContent_UpdatesPropagation()
{
using var ctx = new TestContext();
var component = ctx.RenderComponent<RadzenTabs>(parameters => parameters
.Add(p => p.SelectedIndex, 0)
.Add(p => p.Tabs, TabsFragmentWithContent(("First", "First-Content")))
);
var renderCountBefore = component.RenderCount;
var panel = component.Find("div.rz-tabview-panel");
panel.KeyDown(new KeyboardEventArgs { Key = "Escape", Code = "Escape" });
Assert.True(component.RenderCount > renderCountBefore);
}
[Fact]
public void Tabs_ChangeHandlerThrows_StillRendersSelectedTab()
{
using var ctx = new TestContext();
var component = ctx.RenderComponent<RadzenTabs>(parameters => parameters
.Add(p => p.SelectedIndex, 0)
.Add(p => p.Change, (int _) => throw new System.InvalidOperationException("boom"))
.Add(p => p.Tabs, TabsFragmentWithContent(("First", "First-Content"), ("Second", "Second-Content")))
);
Assert.Contains("First-Content", component.Markup);
var secondTab = component.FindAll("button[role='tab']")[1];
var ex = Record.Exception(() => secondTab.Click());
Assert.NotNull(ex);
Assert.Contains("Second-Content", component.Markup);
Assert.DoesNotContain("First-Content", component.Markup);
}
[Fact]
public void Tabs_NonNavigationKeyDown_DoesNotBlockSubsequentRenders()
{
using var ctx = new TestContext();
var component = ctx.RenderComponent<RadzenTabs>(parameters => parameters
.Add(p => p.SelectedIndex, 0)
.Add(p => p.Tabs, TabsFragmentWithContent(("First", "First-Content"), ("Second", "Second-Content")))
);
Assert.Contains("First-Content", component.Markup);
var wrapper = component.Find("div.rz-tabview");
// Simulate non-navigation keystrokes (e.g. a barcode scanner) on the focused tablist.
// Pre-fix: the second keypress latched shouldRender = false, blocking all later renders.
wrapper.KeyDown(new KeyboardEventArgs { Key = "a", Code = "KeyA" });
wrapper.KeyDown(new KeyboardEventArgs { Key = "b", Code = "KeyB" });
component.SetParametersAndRender(parameters => parameters.Add(p => p.SelectedIndex, 1));
Assert.Contains("Second-Content", component.Markup);
Assert.DoesNotContain("First-Content", component.Markup);
}
}
}