Files
Enciphered.Blazor.UIComponents/Enciphered.Blazor.UIComponents.Tests/SidebarTests.cs
T
2026-04-13 15:08:49 +05:00

591 lines
25 KiB
C#

using Microsoft.Playwright;
namespace Enciphered.Blazor.UIComponents.Tests;
[TestFixture]
public class SidebarTests : PlaywrightTestBase
{
/// <summary>
/// Helper: navigate to home and wait for sidebar JS to initialize.
/// </summary>
private async Task GoHomeAsync()
{
await Page.GotoAsync(BaseUrl, new PageGotoOptions { WaitUntil = WaitUntilState.NetworkIdle });
// Wait for the sidebar JS to apply state (data-state attribute appears on wrapper)
await Page.WaitForSelectorAsync("[data-sidebar-wrapper][data-state]", new PageWaitForSelectorOptions
{
Timeout = 10_000
});
}
// ────────────────────────────────────────────
// Initial render
// ────────────────────────────────────────────
[Test]
public async Task Page_Loads_Successfully()
{
var response = await Page.GotoAsync(BaseUrl);
Assert.That(response, Is.Not.Null);
Assert.That(response!.Status, Is.EqualTo(200));
}
[Test]
public async Task Sidebar_Wrapper_Has_DataState_After_Init()
{
await GoHomeAsync();
var wrapper = Page.Locator("[data-sidebar-wrapper]");
var state = await wrapper.GetAttributeAsync("data-state");
Assert.That(state, Is.Not.Null.And.Not.Empty, "Wrapper should have data-state after JS init");
}
[Test]
public async Task Sidebar_Starts_Expanded_On_Desktop()
{
// Ensure desktop viewport
await Page.SetViewportSizeAsync(1280, 800);
await GoHomeAsync();
var wrapper = Page.Locator("[data-sidebar-wrapper]");
var state = await wrapper.GetAttributeAsync("data-state");
Assert.That(state, Is.EqualTo("expanded"), "Sidebar should start expanded on desktop");
}
[Test]
public async Task Sidebar_Element_Exists()
{
await GoHomeAsync();
var sidebar = Page.Locator("[data-sidebar]");
await Expect(sidebar).ToBeVisibleAsync();
}
[Test]
public async Task Sidebar_Trigger_Exists_On_Header()
{
await GoHomeAsync();
// The sidebar header itself is the trigger on desktop
var trigger = Page.Locator("[data-sidebar-header][data-sidebar-trigger]");
await Expect(trigger).ToBeVisibleAsync();
}
[Test]
public async Task Sidebar_Has_Navigation_Links()
{
await GoHomeAsync();
var sidebar = Page.Locator("[data-sidebar]");
var links = sidebar.Locator("a[href]");
var count = await links.CountAsync();
Assert.That(count, Is.GreaterThanOrEqualTo(3), "Should have at least Home, Counter, Weather links");
}
[Test]
public async Task Sidebar_Has_Footer()
{
await GoHomeAsync();
var footer = Page.Locator("[data-sidebar-footer]");
await Expect(footer).ToBeVisibleAsync();
}
// ────────────────────────────────────────────
// Desktop toggle (collapse / expand)
// ────────────────────────────────────────────
[Test]
public async Task Desktop_Click_Trigger_Collapses_Sidebar()
{
await Page.SetViewportSizeAsync(1280, 800);
await GoHomeAsync();
// Verify starts expanded
var wrapper = Page.Locator("[data-sidebar-wrapper]");
Assert.That(await wrapper.GetAttributeAsync("data-state"), Is.EqualTo("expanded"));
// Click the sidebar header (which is the trigger on desktop)
await Page.Locator("[data-sidebar-header][data-sidebar-trigger]").ClickAsync();
// Wait for state change
await Page.WaitForSelectorAsync("[data-sidebar-wrapper][data-state='collapsed']", new PageWaitForSelectorOptions
{
Timeout = 5_000
});
Assert.That(await wrapper.GetAttributeAsync("data-state"), Is.EqualTo("collapsed"));
}
[Test]
public async Task Desktop_Click_Trigger_Twice_Re_Expands_Sidebar()
{
await Page.SetViewportSizeAsync(1280, 800);
await GoHomeAsync();
var trigger = Page.Locator("[data-sidebar-header][data-sidebar-trigger]");
// Collapse
await trigger.ClickAsync();
await Page.WaitForSelectorAsync("[data-sidebar-wrapper][data-state='collapsed']");
// Expand
await trigger.ClickAsync();
await Page.WaitForSelectorAsync("[data-sidebar-wrapper][data-state='expanded']");
var state = await Page.Locator("[data-sidebar-wrapper]").GetAttributeAsync("data-state");
Assert.That(state, Is.EqualTo("expanded"));
}
[Test]
public async Task Desktop_Collapsed_Sidebar_Has_Reduced_Width()
{
await Page.SetViewportSizeAsync(1280, 800);
await GoHomeAsync();
var sidebar = Page.Locator("[data-sidebar]");
// Get expanded width
var expandedWidth = await sidebar.EvaluateAsync<double>("el => el.getBoundingClientRect().width");
Assert.That(expandedWidth, Is.GreaterThan(100), "Expanded sidebar should be wider than 100px");
// Collapse
await Page.Locator("[data-sidebar-header][data-sidebar-trigger]").ClickAsync();
await Page.WaitForSelectorAsync("[data-sidebar-wrapper][data-state='collapsed']");
// Wait for CSS transition
await Page.WaitForTimeoutAsync(300);
// Get collapsed width
var collapsedWidth = await sidebar.EvaluateAsync<double>("el => el.getBoundingClientRect().width");
Assert.That(collapsedWidth, Is.LessThan(expandedWidth), "Collapsed sidebar should be narrower");
Assert.That(collapsedWidth, Is.LessThanOrEqualTo(60), "Collapsed sidebar should be icon-only width (~3rem = 48px)");
}
[Test]
public async Task Desktop_Collapsed_Hides_Menu_Labels()
{
await Page.SetViewportSizeAsync(1280, 800);
await GoHomeAsync();
// Target only the menu item label spans (inside sidebar-content, not sidebar-header)
var labelSpans = Page.Locator("[data-sidebar-content] a span.truncate");
var countBefore = await labelSpans.CountAsync();
Assert.That(countBefore, Is.GreaterThan(0), "Should have label spans");
// Collapse
await Page.Locator("[data-sidebar-header][data-sidebar-trigger]").ClickAsync();
await Page.WaitForSelectorAsync("[data-sidebar-wrapper][data-state='collapsed']");
await Page.WaitForTimeoutAsync(300);
// Check that label text is hidden (CSS-driven via group-data-[state=collapsed])
for (int i = 0; i < countBefore; i++)
{
var display = await labelSpans.Nth(i).EvaluateAsync<string>("el => getComputedStyle(el).display");
Assert.That(display, Is.EqualTo("none"), $"Label span {i} should have display:none when sidebar is collapsed (got '{display}')");
}
}
[Test]
public async Task Desktop_Collapsed_Hides_Group_Label()
{
await Page.SetViewportSizeAsync(1280, 800);
await GoHomeAsync();
var groupLabel = Page.Locator("[data-sidebar-group-label]");
await Expect(groupLabel).ToBeVisibleAsync();
// Collapse
await Page.Locator("[data-sidebar-header][data-sidebar-trigger]").ClickAsync();
await Page.WaitForSelectorAsync("[data-sidebar-wrapper][data-state='collapsed']");
await Page.WaitForTimeoutAsync(300);
await Expect(groupLabel).Not.ToBeVisibleAsync();
}
// ────────────────────────────────────────────
// Spacer width tracks sidebar state
// ────────────────────────────────────────────
[Test]
public async Task Desktop_Spacer_Width_Changes_On_Collapse()
{
await Page.SetViewportSizeAsync(1280, 800);
await GoHomeAsync();
var spacer = Page.Locator("[data-sidebar-spacer]");
var expandedWidth = await spacer.EvaluateAsync<double>("el => el.getBoundingClientRect().width");
Assert.That(expandedWidth, Is.GreaterThan(100), "Spacer should be wide when expanded");
// Collapse
await Page.Locator("[data-sidebar-header][data-sidebar-trigger]").ClickAsync();
await Page.WaitForSelectorAsync("[data-sidebar-wrapper][data-state='collapsed']");
await Page.WaitForTimeoutAsync(300);
var collapsedWidth = await spacer.EvaluateAsync<double>("el => el.getBoundingClientRect().width");
Assert.That(collapsedWidth, Is.LessThan(expandedWidth), "Spacer should shrink when collapsed");
}
// ────────────────────────────────────────────
// Mobile behavior
// ────────────────────────────────────────────
[Test]
public async Task Mobile_Sidebar_Starts_Closed()
{
await Page.SetViewportSizeAsync(375, 812);
await GoHomeAsync();
var wrapper = Page.Locator("[data-sidebar-wrapper]");
var state = await wrapper.GetAttributeAsync("data-state");
Assert.That(state, Is.EqualTo("collapsed"), "Sidebar should start collapsed on mobile");
var mobile = await wrapper.GetAttributeAsync("data-mobile");
Assert.That(mobile, Is.EqualTo("true"), "data-mobile should be 'true' on small viewport");
}
[Test]
public async Task Mobile_Click_Trigger_Opens_Sidebar_And_Shows_Overlay()
{
await Page.SetViewportSizeAsync(375, 812);
await GoHomeAsync();
// Click the mobile trigger button in the inset (visible only on mobile)
await Page.Locator("[data-sidebar-inset] [data-sidebar-trigger]").ClickAsync();
await Page.WaitForSelectorAsync("[data-sidebar-wrapper][data-state='expanded']");
// Overlay should be visible
var overlay = Page.Locator("[data-sidebar-overlay]");
var display = await overlay.EvaluateAsync<string>("el => getComputedStyle(el).display");
Assert.That(display, Is.Not.EqualTo("none"), "Overlay should be visible when mobile sidebar is open");
}
[Test]
public async Task Mobile_Click_Overlay_Closes_Sidebar()
{
await Page.SetViewportSizeAsync(375, 812);
await GoHomeAsync();
// Open via the inset trigger
await Page.Locator("[data-sidebar-inset] [data-sidebar-trigger]").ClickAsync();
await Page.WaitForSelectorAsync("[data-sidebar-wrapper][data-state='expanded']");
// Click overlay
await Page.Locator("[data-sidebar-overlay]").ClickAsync(new LocatorClickOptions { Force = true });
await Page.WaitForSelectorAsync("[data-sidebar-wrapper][data-state='collapsed']");
var state = await Page.Locator("[data-sidebar-wrapper]").GetAttributeAsync("data-state");
Assert.That(state, Is.EqualTo("collapsed"), "Sidebar should close when overlay is clicked");
}
[Test]
public async Task Mobile_Overlay_Hidden_When_Sidebar_Closed()
{
await Page.SetViewportSizeAsync(375, 812);
await GoHomeAsync();
var overlay = Page.Locator("[data-sidebar-overlay]");
var display = await overlay.EvaluateAsync<string>("el => el.style.display || getComputedStyle(el).display");
Assert.That(display, Is.EqualTo("none"), "Overlay should be hidden when mobile sidebar is closed");
}
// ────────────────────────────────────────────
// Navigation links work
// ────────────────────────────────────────────
[Test]
public async Task Clicking_Counter_Link_Navigates_To_Counter()
{
await Page.SetViewportSizeAsync(1280, 800);
await GoHomeAsync();
var counterLink = Page.Locator("[data-sidebar] a[href='/counter']");
await counterLink.ClickAsync();
await Page.WaitForURLAsync($"{BaseUrl}/counter");
Assert.That(Page.Url, Does.Contain("/counter"));
}
[Test]
public async Task Clicking_Weather_Link_Navigates_To_Weather()
{
await Page.SetViewportSizeAsync(1280, 800);
await GoHomeAsync();
var weatherLink = Page.Locator("[data-sidebar] a[href='/weather']");
await weatherLink.ClickAsync();
await Page.WaitForURLAsync($"{BaseUrl}/weather");
Assert.That(Page.Url, Does.Contain("/weather"));
}
[Test]
public async Task Clicking_Header_Toggles_Sidebar()
{
await Page.SetViewportSizeAsync(1280, 800);
await GoHomeAsync();
// Starts expanded
var wrapper = Page.Locator("[data-sidebar-wrapper]");
Assert.That(await wrapper.GetAttributeAsync("data-state"), Is.EqualTo("expanded"));
// Click the sidebar header (logo area) to collapse
await Page.Locator("[data-sidebar-header]").ClickAsync();
await Page.WaitForSelectorAsync("[data-sidebar-wrapper][data-state='collapsed']");
Assert.That(await wrapper.GetAttributeAsync("data-state"), Is.EqualTo("collapsed"));
// Click again to expand
await Page.Locator("[data-sidebar-header]").ClickAsync();
await Page.WaitForSelectorAsync("[data-sidebar-wrapper][data-state='expanded']");
Assert.That(await wrapper.GetAttributeAsync("data-state"), Is.EqualTo("expanded"));
}
// ────────────────────────────────────────────
// Navigation should NOT change sidebar state
// ────────────────────────────────────────────
[Test]
public async Task Desktop_Collapsed_Sidebar_Stays_Collapsed_After_Navigation()
{
await Page.SetViewportSizeAsync(1280, 800);
await GoHomeAsync();
// Verify starts expanded
var wrapper = Page.Locator("[data-sidebar-wrapper]");
Assert.That(await wrapper.GetAttributeAsync("data-state"), Is.EqualTo("expanded"));
// Collapse the sidebar
await Page.Locator("[data-sidebar-header][data-sidebar-trigger]").ClickAsync();
await Page.WaitForSelectorAsync("[data-sidebar-wrapper][data-state='collapsed']");
Assert.That(await wrapper.GetAttributeAsync("data-state"), Is.EqualTo("collapsed"));
// Click a navigation link (Counter)
var counterLink = Page.Locator("[data-sidebar] a[href='/counter']");
await counterLink.ClickAsync();
// Wait for navigation to complete
await Page.WaitForURLAsync($"{BaseUrl}/counter");
// Wait for the sidebar JS to re-apply state after enhanced navigation
await Page.WaitForSelectorAsync("[data-sidebar-wrapper][data-state]", new PageWaitForSelectorOptions
{
Timeout = 5_000
});
// Give any transitions/scripts time to settle
await Page.WaitForTimeoutAsync(500);
// Sidebar should STILL be collapsed
var stateAfterNav = await wrapper.GetAttributeAsync("data-state");
Assert.That(stateAfterNav, Is.EqualTo("collapsed"),
"Sidebar should remain collapsed after clicking a navigation link");
}
[Test]
public async Task Desktop_Collapsed_Sidebar_Stays_Collapsed_After_Multiple_Navigations()
{
await Page.SetViewportSizeAsync(1280, 800);
await GoHomeAsync();
// Collapse the sidebar
await Page.Locator("[data-sidebar-header][data-sidebar-trigger]").ClickAsync();
await Page.WaitForSelectorAsync("[data-sidebar-wrapper][data-state='collapsed']");
var wrapper = Page.Locator("[data-sidebar-wrapper]");
// Navigate to Counter
await Page.Locator("[data-sidebar] a[href='/counter']").ClickAsync();
await Page.WaitForURLAsync($"{BaseUrl}/counter");
await Page.WaitForSelectorAsync("[data-sidebar-wrapper][data-state]");
await Page.WaitForTimeoutAsync(500);
Assert.That(await wrapper.GetAttributeAsync("data-state"), Is.EqualTo("collapsed"),
"Sidebar should remain collapsed after navigating to Counter");
// Navigate to Weather
await Page.Locator("[data-sidebar] a[href='/weather']").ClickAsync();
await Page.WaitForURLAsync($"{BaseUrl}/weather");
await Page.WaitForSelectorAsync("[data-sidebar-wrapper][data-state]");
await Page.WaitForTimeoutAsync(500);
Assert.That(await wrapper.GetAttributeAsync("data-state"), Is.EqualTo("collapsed"),
"Sidebar should remain collapsed after navigating to Weather");
// Navigate back Home
await Page.Locator("[data-sidebar] a[href='/']").ClickAsync();
await Page.WaitForURLAsync(url => url == BaseUrl || url == BaseUrl + "/");
await Page.WaitForSelectorAsync("[data-sidebar-wrapper][data-state]");
await Page.WaitForTimeoutAsync(500);
Assert.That(await wrapper.GetAttributeAsync("data-state"), Is.EqualTo("collapsed"),
"Sidebar should remain collapsed after navigating back to Home");
}
[Test]
public async Task Desktop_Sidebar_Toggle_Works_After_Same_Page_Navigation()
{
await Page.SetViewportSizeAsync(1280, 800);
await GoHomeAsync();
var wrapper = Page.Locator("[data-sidebar-wrapper]");
// Starts expanded
Assert.That(await wrapper.GetAttributeAsync("data-state"), Is.EqualTo("expanded"));
// Click the Home link while already on home (same-page navigation)
await Page.Locator("[data-sidebar] a[href='/']").ClickAsync();
// Wait for any enhanced navigation / DOM mutation to settle
await Page.WaitForSelectorAsync("[data-sidebar-wrapper][data-state]", new PageWaitForSelectorOptions
{
Timeout = 5_000
});
await Page.WaitForTimeoutAsync(500);
// Now click the sidebar trigger to collapse
await Page.Locator("[data-sidebar-header][data-sidebar-trigger]").ClickAsync();
await Page.WaitForTimeoutAsync(300);
var stateAfterFirstToggle = await wrapper.GetAttributeAsync("data-state");
Assert.That(stateAfterFirstToggle, Is.EqualTo("collapsed"),
"Sidebar should be collapsed after one trigger click following same-page nav");
// Click the sidebar trigger again to expand
await Page.Locator("[data-sidebar-header][data-sidebar-trigger]").ClickAsync();
await Page.WaitForTimeoutAsync(300);
var stateAfterSecondToggle = await wrapper.GetAttributeAsync("data-state");
Assert.That(stateAfterSecondToggle, Is.EqualTo("expanded"),
"Sidebar should be expanded after second trigger click following same-page nav");
}
[Test]
public async Task Desktop_Collapsed_Sidebar_Stays_Collapsed_After_Same_Page_Navigation()
{
await Page.SetViewportSizeAsync(1280, 800);
await GoHomeAsync();
var wrapper = Page.Locator("[data-sidebar-wrapper]");
// Collapse the sidebar first
await Page.Locator("[data-sidebar-header][data-sidebar-trigger]").ClickAsync();
await Page.WaitForSelectorAsync("[data-sidebar-wrapper][data-state='collapsed']");
Assert.That(await wrapper.GetAttributeAsync("data-state"), Is.EqualTo("collapsed"));
// Click the Home link while already on home (same-page navigation)
await Page.Locator("[data-sidebar] a[href='/']").ClickAsync();
// Wait for any enhanced navigation / DOM mutation to settle
await Page.WaitForTimeoutAsync(500);
// Sidebar should STILL be collapsed
Assert.That(await wrapper.GetAttributeAsync("data-state"), Is.EqualTo("collapsed"),
"Sidebar should remain collapsed after same-page navigation");
// Toggle should still work correctly: collapse -> expand
await Page.Locator("[data-sidebar-header][data-sidebar-trigger]").ClickAsync();
await Page.WaitForTimeoutAsync(300);
Assert.That(await wrapper.GetAttributeAsync("data-state"), Is.EqualTo("expanded"),
"Trigger should expand sidebar after same-page nav while collapsed");
// And back to collapsed
await Page.Locator("[data-sidebar-header][data-sidebar-trigger]").ClickAsync();
await Page.WaitForTimeoutAsync(300);
Assert.That(await wrapper.GetAttributeAsync("data-state"), Is.EqualTo("collapsed"),
"Trigger should collapse sidebar again after same-page nav");
}
// ────────────────────────────────────────────
// Cookie persistence
// ────────────────────────────────────────────
[Test]
public async Task Desktop_State_Persists_Via_Cookie()
{
await Page.SetViewportSizeAsync(1280, 800);
await GoHomeAsync();
// Collapse
await Page.Locator("[data-sidebar-header][data-sidebar-trigger]").ClickAsync();
await Page.WaitForSelectorAsync("[data-sidebar-wrapper][data-state='collapsed']");
// Check cookie
var cookies = await Context.CookiesAsync();
var sidebarCookie = cookies.FirstOrDefault(c => c.Name == "sidebar:state");
Assert.That(sidebarCookie, Is.Not.Null, "sidebar:state cookie should exist");
Assert.That(sidebarCookie!.Value, Is.EqualTo("closed"), "Cookie should be 'closed' after collapse");
// Reload page — sidebar should remain collapsed
await Page.ReloadAsync(new PageReloadOptions { WaitUntil = WaitUntilState.NetworkIdle });
await Page.WaitForSelectorAsync("[data-sidebar-wrapper][data-state]");
var state = await Page.Locator("[data-sidebar-wrapper]").GetAttributeAsync("data-state");
Assert.That(state, Is.EqualTo("collapsed"), "Sidebar state should persist after reload");
}
// ────────────────────────────────────────────
// Viewport resize transitions
// ────────────────────────────────────────────
[Test]
public async Task Resize_From_Desktop_To_Mobile_Collapses_Sidebar()
{
await Page.SetViewportSizeAsync(1280, 800);
await GoHomeAsync();
var wrapper = Page.Locator("[data-sidebar-wrapper]");
Assert.That(await wrapper.GetAttributeAsync("data-state"), Is.EqualTo("expanded"));
// Shrink to mobile
await Page.SetViewportSizeAsync(375, 812);
// Wait for resize handler to fire
await Page.WaitForSelectorAsync("[data-sidebar-wrapper][data-mobile='true']", new PageWaitForSelectorOptions
{
Timeout = 5_000
});
Assert.That(await wrapper.GetAttributeAsync("data-state"), Is.EqualTo("collapsed"));
Assert.That(await wrapper.GetAttributeAsync("data-mobile"), Is.EqualTo("true"));
}
[Test]
public async Task Resize_From_Mobile_To_Desktop_Expands_Sidebar()
{
await Page.SetViewportSizeAsync(375, 812);
await GoHomeAsync();
var wrapper = Page.Locator("[data-sidebar-wrapper]");
Assert.That(await wrapper.GetAttributeAsync("data-state"), Is.EqualTo("collapsed"));
// Grow to desktop
await Page.SetViewportSizeAsync(1280, 800);
await Page.WaitForSelectorAsync("[data-sidebar-wrapper][data-mobile='false']", new PageWaitForSelectorOptions
{
Timeout = 5_000
});
Assert.That(await wrapper.GetAttributeAsync("data-state"), Is.EqualTo("expanded"));
Assert.That(await wrapper.GetAttributeAsync("data-mobile"), Is.EqualTo("false"));
}
// ────────────────────────────────────────────
// Sidebar inset (main content area)
// ────────────────────────────────────────────
[Test]
public async Task SidebarInset_Exists_And_Contains_Page_Content()
{
await GoHomeAsync();
var inset = Page.Locator("[data-sidebar-inset]");
await Expect(inset).ToBeVisibleAsync();
// On mobile, the inset should contain the trigger button
await Page.SetViewportSizeAsync(375, 812);
await GoHomeAsync();
var mobileTrigger = inset.Locator("[data-sidebar-trigger]");
await Expect(mobileTrigger).ToBeVisibleAsync();
}
}