feat(centralui): collapsible sidebar nav sections
Make the seven sidebar section groups (Admin, Design, Deployment, Notifications, Site Calls, Monitoring, Audit) collapsible. New NavSection component renders a header toggle button (chevron) and reveals its items only while expanded; NavMenu owns the expanded-section set. Behaviour: sections are collapsed by default; state persists in the `scadabridge_nav` cookie (written/read via the new nav-state.js JS interop, mirroring treeview-storage.js) so it survives reloads and reconnects; navigating into a section auto-expands it and remembers it. The Dashboard item stays sectionless and always visible. Tests: NavMenu bUnit tests expand sections before asserting items and add collapsed-by-default / toggle / cookie-persistence cases; Playwright nav tests expand sections before clicking links; new NavCollapseTests covers the feature E2E. Build 0 warnings; bUnit 545 passed; Playwright nav suite green (the unrelated AuditGridColumnTests resize-reload case remains pre-existing flaky — an un-awaited save race in that test).
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
using Microsoft.Playwright;
|
||||
|
||||
namespace ScadaLink.CentralUI.PlaywrightTests;
|
||||
|
||||
/// <summary>
|
||||
/// E2E tests for the collapsible sidebar nav sections: sections are collapsed
|
||||
/// by default, a header toggle reveals a section's items, the state persists in
|
||||
/// the <c>scadabridge_nav</c> cookie across a full page reload, and navigating
|
||||
/// into a section auto-expands it.
|
||||
/// </summary>
|
||||
[Collection("Playwright")]
|
||||
public class NavCollapseTests
|
||||
{
|
||||
private readonly PlaywrightFixture _fixture;
|
||||
|
||||
public NavCollapseTests(PlaywrightFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Sections_AreCollapsedByDefault_AfterLogin()
|
||||
{
|
||||
var page = await _fixture.NewAuthenticatedPageAsync();
|
||||
|
||||
// The dashboard is sectionless, so no section is auto-expanded and the
|
||||
// cookie is empty on a fresh context — every section toggle is collapsed.
|
||||
await Expect(page.Locator("button.nav-section-toggle[aria-expanded='true']"))
|
||||
.ToHaveCountAsync(0);
|
||||
// A sectioned link is therefore absent from the DOM.
|
||||
Assert.Equal(0, await page.Locator("nav a:has-text('Topology')").CountAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ClickingSectionHeader_RevealsItsItems()
|
||||
{
|
||||
var page = await _fixture.NewAuthenticatedPageAsync();
|
||||
var toggle = page.Locator("button.nav-section-toggle:has-text('Deployment')");
|
||||
|
||||
Assert.Equal(0, await page.Locator("nav a:has-text('Topology')").CountAsync());
|
||||
|
||||
await toggle.ClickAsync();
|
||||
|
||||
await Expect(toggle).ToHaveAttributeAsync("aria-expanded", "true");
|
||||
await Expect(page.GetByRole(AriaRole.Link, new() { Name = "Topology" }))
|
||||
.ToBeVisibleAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CollapseState_SurvivesPageReload()
|
||||
{
|
||||
var page = await _fixture.NewAuthenticatedPageAsync();
|
||||
await page.Locator("button.nav-section-toggle:has-text('Deployment')").ClickAsync();
|
||||
await Expect(page.GetByRole(AriaRole.Link, new() { Name = "Topology" }))
|
||||
.ToBeVisibleAsync();
|
||||
|
||||
await page.ReloadAsync();
|
||||
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||
|
||||
// The scadabridge_nav cookie restored the expanded Deployment section.
|
||||
await Expect(page.Locator("button.nav-section-toggle:has-text('Deployment')"))
|
||||
.ToHaveAttributeAsync("aria-expanded", "true");
|
||||
await Expect(page.GetByRole(AriaRole.Link, new() { Name = "Topology" }))
|
||||
.ToBeVisibleAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NavigatingIntoCollapsedSection_AutoExpandsIt()
|
||||
{
|
||||
var page = await _fixture.NewAuthenticatedPageAsync();
|
||||
var auditToggle = page.Locator("button.nav-section-toggle:has-text('Audit')");
|
||||
|
||||
// The Audit section starts collapsed.
|
||||
await Expect(auditToggle).ToHaveAttributeAsync("aria-expanded", "false");
|
||||
|
||||
// Navigate into the Audit section via an in-page link (SPA navigation,
|
||||
// which raises NavigationManager.LocationChanged) — the Configuration
|
||||
// Audit Log quick-action card on the dashboard.
|
||||
await page.Locator("a[href='/audit/configuration']").First.ClickAsync();
|
||||
await PlaywrightFixture.WaitForPathAsync(page, "/audit/configuration");
|
||||
|
||||
// The Audit nav section auto-expanded on arrival.
|
||||
await Expect(auditToggle).ToHaveAttributeAsync("aria-expanded", "true");
|
||||
}
|
||||
|
||||
private static ILocatorAssertions Expect(ILocator locator) =>
|
||||
Assertions.Expect(locator);
|
||||
}
|
||||
@@ -77,6 +77,8 @@ public class NavigationTests
|
||||
|
||||
private static async Task ClickNavAndWait(IPage page, string linkText, string expectedPath)
|
||||
{
|
||||
// Sections are collapsed by default — open them so the link is in the DOM.
|
||||
await PlaywrightFixture.ExpandAllNavSectionsAsync(page);
|
||||
await page.Locator($"nav a:has-text('{linkText}')").ClickAsync();
|
||||
await PlaywrightFixture.WaitForPathAsync(page, expectedPath);
|
||||
Assert.Contains(expectedPath, page.Url);
|
||||
|
||||
@@ -105,6 +105,28 @@ public class PlaywrightFixture : IAsyncLifetime
|
||||
: $"() => window.location.pathname.includes('{path}')";
|
||||
await page.WaitForFunctionAsync(js, null, new() { Timeout = timeoutMs });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Expand every collapsed sidebar nav section. Nav sections are collapsed by
|
||||
/// default, so a section's links are not in the DOM until it is expanded.
|
||||
/// Call this after authenticating, before interacting with sectioned nav links.
|
||||
/// </summary>
|
||||
public static async Task ExpandAllNavSectionsAsync(IPage page)
|
||||
{
|
||||
var toggles = page.Locator("button.nav-section-toggle");
|
||||
int count = await toggles.CountAsync();
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var toggle = toggles.Nth(i);
|
||||
if (await toggle.GetAttributeAsync("aria-expanded") == "false")
|
||||
{
|
||||
await toggle.ClickAsync();
|
||||
// Wait for the toggle's own state to flip so the Blazor
|
||||
// re-render has landed before moving to the next section.
|
||||
await Assertions.Expect(toggle).ToHaveAttributeAsync("aria-expanded", "true");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[CollectionDefinition("Playwright")]
|
||||
|
||||
@@ -220,6 +220,9 @@ public class RoleNavigationTests
|
||||
|
||||
private static async Task AssertNavLinkVisible(IPage page, string linkText)
|
||||
{
|
||||
// Sections are collapsed by default — expand them so a present link is
|
||||
// in the DOM. Idempotent: already-expanded sections are skipped.
|
||||
await PlaywrightFixture.ExpandAllNavSectionsAsync(page);
|
||||
var locator = page.Locator($"nav a:has-text('{linkText}')");
|
||||
var count = await locator.CountAsync();
|
||||
Assert.True(count > 0, $"Expected nav link '{linkText}' to be visible, but it was not found");
|
||||
|
||||
Reference in New Issue
Block a user