using Microsoft.Playwright; namespace Enciphered.Blazor.UIComponents.Tests; [TestFixture] public class SidebarTests : PlaywrightTestBase { /// /// Helper: navigate to home and wait for sidebar JS to initialize. /// 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("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("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("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("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("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("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("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(); } }