Basics Done

This commit is contained in:
2026-04-13 15:08:49 +05:00
parent 9bef5813ae
commit 06ec22704b
75 changed files with 5036 additions and 2733 deletions
@@ -0,0 +1,324 @@
using Microsoft.Playwright;
namespace Enciphered.Blazor.UIComponents.Tests;
[TestFixture]
public class ThemeToggleTests : PlaywrightTestBase
{
/// <summary>
/// Navigate to home, wait for sidebar JS + darkmode JS to finish initializing.
/// </summary>
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
});
}
/// <summary>
/// Click the toggle and wait for the dark class to be added to &lt;html&gt;.
/// </summary>
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 });
}
/// <summary>
/// Click the toggle and wait for the dark class to be removed from &lt;html&gt;.
/// </summary>
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<bool>(
"() => 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<string>("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<string>("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<bool>(
"() => document.documentElement.classList.contains('dark')");
Assert.That(hasDark, Is.True, "Clicking toggle should add 'dark' class to <html>");
}
[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<string>("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<string>("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<string>("() => 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<bool>(
"() => document.documentElement.classList.contains('dark')");
Assert.That(hasDark, Is.False, "Double-clicking toggle should return to light mode");
var stored = await Page.EvaluateAsync<string>("() => 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<string>("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<string>("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 <script> in <head> should apply 'dark' before paint
var hasDark = await Page.EvaluateAsync<bool>(
"() => document.documentElement.classList.contains('dark')");
Assert.That(hasDark, Is.True, "Dark mode should persist after page reload");
}
[Test]
public async Task Dark_Mode_Persists_After_Navigation()
{
await GoHomeAsync();
await ToggleToDarkAsync();
// Navigate to counter page via sidebar link
var counterLink = Page.Locator("[data-sidebar] a[href='/counter']");
await counterLink.ClickAsync();
await Page.WaitForURLAsync($"{BaseUrl}/counter");
// Wait for the page to fully settle after enhanced navigation
await Page.WaitForLoadStateAsync(LoadState.NetworkIdle);
// Dark class should still be present on <html>
var hasDark = await Page.EvaluateAsync<bool>(
"() => document.documentElement.classList.contains('dark')");
Assert.That(hasDark, Is.True, "Dark mode should persist across navigation");
// localStorage should also still have 'dark'
var stored = await Page.EvaluateAsync<string>("() => localStorage.getItem('theme')");
Assert.That(stored, Is.EqualTo("dark"), "localStorage should still be 'dark' after navigation");
}
[Test]
public async Task Light_Mode_Persists_After_Reload()
{
await GoHomeAsync();
// Toggle to dark then back to light
await ToggleToDarkAsync();
await ToggleToLightAsync();
// Reload
await Page.ReloadAsync(new PageReloadOptions { WaitUntil = WaitUntilState.NetworkIdle });
var hasDark = await Page.EvaluateAsync<bool>(
"() => document.documentElement.classList.contains('dark')");
Assert.That(hasDark, Is.False, "Light mode should persist after page reload");
}
// ────────────────────────────────────────────
// No FOUC (flash of unstyled content)
// ────────────────────────────────────────────
[Test]
public async Task Dark_Mode_Applied_Before_First_Paint_No_FOUC()
{
await GoHomeAsync();
await ToggleToDarkAsync();
// Reload and check at DOMContentLoaded — the inline script should have already set .dark
await Page.ReloadAsync(new PageReloadOptions { WaitUntil = WaitUntilState.DOMContentLoaded });
var hasDarkImmediately = await Page.EvaluateAsync<bool>(
"() => document.documentElement.classList.contains('dark')");
Assert.That(hasDarkImmediately, Is.True,
"Dark class should be on <html> immediately on DOMContentLoaded (no FOUC)");
}
// ────────────────────────────────────────────
// Visual theming verification
// ────────────────────────────────────────────
[Test]
public async Task Dark_Mode_Changes_Background_Color()
{
await Page.SetViewportSizeAsync(1280, 800);
await GoHomeAsync();
// Read the --background CSS custom property which drives bg-background
var lightBgVar = await Page.EvaluateAsync<string>(
"() => getComputedStyle(document.documentElement).getPropertyValue('--background').trim()");
// Toggle to dark
await ToggleToDarkAsync();
await Page.WaitForTimeoutAsync(200);
var darkBgVar = await Page.EvaluateAsync<string>(
"() => getComputedStyle(document.documentElement).getPropertyValue('--background').trim()");
Assert.That(darkBgVar, Is.Not.EqualTo(lightBgVar),
$"--background CSS variable should change in dark mode. Light={lightBgVar}, Dark={darkBgVar}");
}
[Test]
public async Task Dark_Mode_Changes_Sidebar_Background()
{
await Page.SetViewportSizeAsync(1280, 800);
await GoHomeAsync();
var sidebar = Page.Locator("[data-sidebar]");
var lightSidebarBg = await sidebar.EvaluateAsync<string>(
"el => getComputedStyle(el).backgroundColor");
// Toggle to dark
await ToggleToDarkAsync();
await Page.WaitForTimeoutAsync(200);
var darkSidebarBg = await sidebar.EvaluateAsync<string>(
"el => getComputedStyle(el).backgroundColor");
Assert.That(darkSidebarBg, Is.Not.EqualTo(lightSidebarBg),
"Sidebar background should change in dark mode");
}
// ────────────────────────────────────────────
// Button styling (shadcn ghost button)
// ────────────────────────────────────────────
[Test]
public async Task ThemeToggle_Has_Correct_Dimensions()
{
await GoHomeAsync();
var toggle = Page.Locator("[data-theme-toggle]");
var box = await toggle.BoundingBoxAsync();
Assert.That(box, Is.Not.Null, "Toggle button should have a bounding box");
// h-9 w-9 = 36px × 36px
Assert.That(box!.Width, Is.InRange(34, 38), "Toggle button width should be ~36px (h-9)");
Assert.That(box.Height, Is.InRange(34, 38), "Toggle button height should be ~36px (w-9)");
}
[Test]
public async Task ThemeToggle_Contains_Exactly_Two_Svg_Icons()
{
await GoHomeAsync();
var toggle = Page.Locator("[data-theme-toggle]");
var svgCount = await toggle.Locator("svg").CountAsync();
Assert.That(svgCount, Is.EqualTo(2),
"Toggle should contain exactly two SVG icons (moon and sun)");
}
}