using Microsoft.Playwright; namespace Enciphered.Blazor.UIComponents.Tests; [TestFixture] public class ThemeToggleTests : PlaywrightTestBase { /// /// Navigate to home, wait for sidebar JS + darkmode JS to finish initializing. /// private async Task GoHomeAsync() { await Page.GotoAsync(BaseUrl, new PageGotoOptions { WaitUntil = WaitUntilState.NetworkIdle }); // Wait for sidebar JS to apply state await Page.WaitForSelectorAsync("[data-sidebar-wrapper][data-state]", new PageWaitForSelectorOptions { Timeout = 10_000 }); // Wait for the ThemeToggle button to appear await Page.WaitForSelectorAsync("[data-theme-toggle]", new PageWaitForSelectorOptions { Timeout = 10_000 }); } /// /// Click the toggle and wait for the dark class to be added to <html>. /// private async Task ToggleToDarkAsync() { await Page.Locator("[data-theme-toggle]").ClickAsync(); await Page.WaitForFunctionAsync( "() => document.documentElement.classList.contains('dark')", null, new PageWaitForFunctionOptions { Timeout = 5_000 }); } /// /// Click the toggle and wait for the dark class to be removed from <html>. /// private async Task ToggleToLightAsync() { await Page.Locator("[data-theme-toggle]").ClickAsync(); await Page.WaitForFunctionAsync( "() => !document.documentElement.classList.contains('dark')", null, new PageWaitForFunctionOptions { Timeout = 5_000 }); } // ──────────────────────────────────────────── // Initial render // ──────────────────────────────────────────── [Test] public async Task ThemeToggle_Button_Is_Visible() { await GoHomeAsync(); var toggle = Page.Locator("[data-theme-toggle]"); await Expect(toggle).ToBeVisibleAsync(); } [Test] public async Task ThemeToggle_Starts_In_Light_Mode_By_Default() { await GoHomeAsync(); var hasDark = await Page.EvaluateAsync( "() => document.documentElement.classList.contains('dark')"); Assert.That(hasDark, Is.False, "Page should start in light mode when no preference is stored"); } [Test] public async Task ThemeToggle_Shows_Moon_Icon_In_Light_Mode() { await GoHomeAsync(); var moon = Page.Locator("[data-theme-toggle] [data-theme-icon-moon]"); var moonDisplay = await moon.EvaluateAsync("el => getComputedStyle(el).display"); Assert.That(moonDisplay, Is.Not.EqualTo("none"), "Moon icon should be visible in light mode"); var sun = Page.Locator("[data-theme-toggle] [data-theme-icon-sun]"); var sunDisplay = await sun.EvaluateAsync("el => el.style.display"); Assert.That(sunDisplay, Is.EqualTo("none"), "Sun icon should be hidden in light mode"); } // ──────────────────────────────────────────── // Toggle to dark mode // ──────────────────────────────────────────── [Test] public async Task Click_Toggle_Adds_Dark_Class_To_Html() { await GoHomeAsync(); await ToggleToDarkAsync(); var hasDark = await Page.EvaluateAsync( "() => document.documentElement.classList.contains('dark')"); Assert.That(hasDark, Is.True, "Clicking toggle should add 'dark' class to "); } [Test] public async Task Click_Toggle_Shows_Sun_Icon_In_Dark_Mode() { await GoHomeAsync(); await ToggleToDarkAsync(); var sun = Page.Locator("[data-theme-toggle] [data-theme-icon-sun]"); var sunDisplay = await sun.EvaluateAsync("el => el.style.display"); Assert.That(sunDisplay, Is.Not.EqualTo("none"), "Sun icon should be visible in dark mode"); var moon = Page.Locator("[data-theme-toggle] [data-theme-icon-moon]"); var moonDisplay = await moon.EvaluateAsync("el => el.style.display"); Assert.That(moonDisplay, Is.EqualTo("none"), "Moon icon should be hidden in dark mode"); } [Test] public async Task Click_Toggle_Stores_Dark_In_LocalStorage() { await GoHomeAsync(); await ToggleToDarkAsync(); var stored = await Page.EvaluateAsync("() => localStorage.getItem('theme')"); Assert.That(stored, Is.EqualTo("dark"), "localStorage 'theme' should be 'dark' after toggle"); } // ──────────────────────────────────────────── // Toggle back to light mode // ──────────────────────────────────────────── [Test] public async Task Double_Click_Toggle_Returns_To_Light_Mode() { await GoHomeAsync(); // Toggle to dark await ToggleToDarkAsync(); // Toggle back to light await ToggleToLightAsync(); var hasDark = await Page.EvaluateAsync( "() => document.documentElement.classList.contains('dark')"); Assert.That(hasDark, Is.False, "Double-clicking toggle should return to light mode"); var stored = await Page.EvaluateAsync("() => localStorage.getItem('theme')"); Assert.That(stored, Is.EqualTo("light"), "localStorage should be 'light' after toggling back"); } [Test] public async Task Double_Click_Toggle_Shows_Moon_Icon_Again() { await GoHomeAsync(); // Dark await ToggleToDarkAsync(); // Light again await ToggleToLightAsync(); var moon = Page.Locator("[data-theme-toggle] [data-theme-icon-moon]"); var moonDisplay = await moon.EvaluateAsync("el => getComputedStyle(el).display"); Assert.That(moonDisplay, Is.Not.EqualTo("none"), "Moon icon should be visible again after toggling back"); var sun = Page.Locator("[data-theme-toggle] [data-theme-icon-sun]"); var sunDisplay = await sun.EvaluateAsync("el => el.style.display"); Assert.That(sunDisplay, Is.EqualTo("none"), "Sun icon should be hidden again after toggling back"); } // ──────────────────────────────────────────── // Persistence across page reloads // ──────────────────────────────────────────── [Test] public async Task Dark_Mode_Persists_After_Reload() { await GoHomeAsync(); await ToggleToDarkAsync(); // Reload the page await Page.ReloadAsync(new PageReloadOptions { WaitUntil = WaitUntilState.NetworkIdle }); // The inline