using Microsoft.Playwright; namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests; /// /// E2E tests for the collapsible sidebar nav sections, as implemented by the /// ZB.MOM.WW.Theme kit (NavRailSection). Each section is a native /// <details class="rail-section"> with a /// <summary class="rail-eyebrow-toggle"> toggle whose /// aria-expanded mirrors the open state. Sections are expanded by /// default; clicking a header toggles it; the open/closed state persists in /// localStorage (key zbnav:<sectionKey>) across a full reload; /// and the section holding the active link is auto-revealed on arrival. /// /// The kit nav is a static-SSR / CSS-only design. ScadaBridge's Central UI /// renders under global @rendermode InteractiveServer, where the kit's /// collapse is currently non-functional — Blazor's management of the native /// <details> defeats the content-hiding and nav-state.js never /// wires the live DOM (no data-zbnav-initialized), so aria sync, localStorage /// persistence, and active-reveal are inert. See themeissues.md Issue 6 in the /// ZB.MOM.WW.Theme repo. The three behavior tests below assert the corrected /// behavior and are -skipped until the kit fix /// (hide-when-closed CSS + interactive re-wire) is built and the cluster redeployed, /// at which point they run and must pass. /// [Collection("Playwright")] public class NavCollapseTests { private const string KitNavSkipReason = "ZB.MOM.WW.Theme collapsible nav is non-functional under interactive Blazor render " + "(see themeissues.md Issue 6); skipped pending the kit fix + cluster redeploy."; private readonly PlaywrightFixture _fixture; public NavCollapseTests(PlaywrightFixture fixture) { _fixture = fixture; } [Fact] public async Task Sections_AreExpandedByDefault_AfterLogin() { var page = await _fixture.NewAuthenticatedPageAsync(); // On a fresh context there is no saved state, so every section renders // expanded (NavRailSection defaults Expanded=true) — no section toggle // reports aria-expanded="false". This holds regardless of whether the kit's // JS collapse is wired, so it is a plain (non-skippable) fact. await Expect(page.Locator("summary.rail-eyebrow-toggle[aria-expanded='false']")) .ToHaveCountAsync(0); // A sectioned link is therefore visible without any expansion. await Expect(page.Locator("a.rail-link:has-text('Topology')")).ToBeVisibleAsync(); } [SkippableFact] public async Task ClickingSectionHeader_TogglesItsItems() { var page = await _fixture.NewAuthenticatedPageAsync(); Skip.IfNot(await NavCollapseWiredAsync(page), KitNavSkipReason); var toggle = page.Locator("summary.rail-eyebrow-toggle:has-text('Deployment')"); var topology = page.Locator("a.rail-link:has-text('Topology')"); // Starts expanded. await Expect(toggle).ToHaveAttributeAsync("aria-expanded", "true"); await Expect(topology).ToBeVisibleAsync(); // Clicking the header collapses the section and hides its items. await toggle.ClickAsync(); await Expect(toggle).ToHaveAttributeAsync("aria-expanded", "false"); await Expect(topology).Not.ToBeVisibleAsync(); // Clicking again re-expands it. await toggle.ClickAsync(); await Expect(toggle).ToHaveAttributeAsync("aria-expanded", "true"); await Expect(topology).ToBeVisibleAsync(); } [SkippableFact] public async Task CollapseState_SurvivesPageReload() { var page = await _fixture.NewAuthenticatedPageAsync(); Skip.IfNot(await NavCollapseWiredAsync(page), KitNavSkipReason); var toggle = page.Locator("summary.rail-eyebrow-toggle:has-text('Deployment')"); await toggle.ClickAsync(); await Expect(toggle).ToHaveAttributeAsync("aria-expanded", "false"); await page.ReloadAsync(); await page.WaitForLoadStateAsync(LoadState.NetworkIdle); // nav-state.js restored the collapsed Deployment section from the // zbnav:deployment localStorage entry. (The dashboard has no active link // inside Deployment, so the active-reveal does not re-open it.) await Expect(page.Locator("summary.rail-eyebrow-toggle:has-text('Deployment')")) .ToHaveAttributeAsync("aria-expanded", "false"); await Expect(page.Locator("a.rail-link:has-text('Topology')")).Not.ToBeVisibleAsync(); } [SkippableFact] public async Task NavigatingIntoCollapsedSection_AutoExpandsIt() { var page = await _fixture.NewAuthenticatedPageAsync(); Skip.IfNot(await NavCollapseWiredAsync(page), KitNavSkipReason); var auditToggle = page.Locator("summary.rail-eyebrow-toggle:has-text('Audit')"); // Collapse the Audit section and confirm the preference is persisted. await Expect(auditToggle).ToHaveAttributeAsync("aria-expanded", "true"); await auditToggle.ClickAsync(); await Expect(auditToggle).ToHaveAttributeAsync("aria-expanded", "false"); // Navigate to a route whose link lives in the now-collapsed Audit section. // On load the kit restores Audit collapsed from localStorage, then the // active-link reveal force-opens it so the nav shows where the user is. await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/audit/log"); await page.WaitForLoadStateAsync(LoadState.NetworkIdle); await Expect(page.Locator("summary.rail-eyebrow-toggle:has-text('Audit')")) .ToHaveAttributeAsync("aria-expanded", "true"); await Expect(page.Locator("details.rail-section a.rail-link.active")) .ToBeVisibleAsync(); // The reveal is transient: it must NOT overwrite the user's saved collapse // preference, which stays "0" in localStorage. var saved = await page.EvaluateAsync( "() => window.localStorage.getItem('zbnav:audit')"); Assert.Equal("0", saved); } /// /// Returns true once the kit's nav-state.js has wired the live (interactive) /// nav — i.e. every details.rail-section carries data-zbnav-initialized. /// That is the observable signal that the themeissues.md Issue 6 fix is deployed; until /// then the collapse is inert and the behavior tests skip. Polls briefly to allow the /// interactive circuit to re-wire after render. /// private static async Task NavCollapseWiredAsync(IPage page) { const string probe = "() => { const d = document.querySelectorAll('details.rail-section');" + " return d.length > 0 && [...d].every(x => x.hasAttribute('data-zbnav-initialized')); }"; for (int i = 0; i < 20; i++) { if (await page.EvaluateAsync(probe)) return true; await page.WaitForTimeoutAsync(250); } return false; } private static ILocatorAssertions Expect(ILocator locator) => Assertions.Expect(locator); }