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);
}