-
Notifications
You must be signed in to change notification settings - Fork 0
test: add component coverage gate and auth UI tests #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,23 +1,22 @@ | ||
| @inherits LayoutComponentBase | ||
|
|
||
| <div class="page"> | ||
| <div class="sidebar"> | ||
| <NavMenu /> | ||
| </div> | ||
|
|
||
| <main> | ||
| <div class="top-row px-4"> | ||
| <a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a> | ||
| </div> | ||
| <div class="min-h-screen flex flex-col bg-primary-400 dark:bg-primary-800 text-primary-800 dark:text-primary-300"> | ||
| <NavMenu/> | ||
|
|
||
| <article class="content px-4"> | ||
| @Body | ||
| </article> | ||
| </main> | ||
| <main id="main-content" tabindex="-1" class="mx-auto w-full max-w-7xl flex-1 px-4 pt-20 pb-8 outline-none focus:outline-none focus-visible:outline-none"> | ||
| @Body | ||
| </main> | ||
|
|
||
| <footer class="mt-auto"> | ||
| <div class="mx-auto w-full max-w-7xl px-4"> | ||
| © @DateTime.Now.Year MyBlog — Training Project | ||
| </div> | ||
| </footer> | ||
| </div> | ||
|
|
||
| <div id="blazor-error-ui" data-nosnippet> | ||
| An unhandled error has occurred. | ||
| <a href="." class="reload">Reload</a> | ||
| <span class="dismiss">🗙</span> | ||
| An unhandled error has occurred. | ||
| <a href="." class="reload">Reload</a> | ||
| <span class="dismiss">🗙</span> | ||
| </div> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,62 +1,200 @@ | ||
| @using Microsoft.AspNetCore.Components.Authorization | ||
| @inject IJSRuntime Js | ||
| @inject NavigationManager Nav | ||
| @implements IDisposable | ||
| @rendermode InteractiveServer | ||
|
|
||
| <div class="top-row ps-3 navbar navbar-dark"> | ||
| <div class="container-fluid"> | ||
| <a class="navbar-brand" href="">MyBlog</a> | ||
| </div> | ||
| </div> | ||
|
|
||
| <input type="checkbox" title="Navigation menu" class="navbar-toggler" /> | ||
|
|
||
| <div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()"> | ||
| <nav class="nav flex-column"> | ||
| <div class="nav-item px-3"> | ||
| <NavLink class="nav-link" href="" Match="NavLinkMatch.All"> | ||
| <span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home | ||
| </NavLink> | ||
| </div> | ||
|
|
||
| <div class="nav-item px-3"> | ||
| <NavLink class="nav-link" href="blog"> | ||
| <span class="bi bi-journal-text-nav-menu" aria-hidden="true"></span> Blog Posts | ||
| </NavLink> | ||
| </div> | ||
|
|
||
| <AuthorizeView Roles="Author,Admin"> | ||
| <Authorized> | ||
| <div class="nav-item px-3"> | ||
| <NavLink class="nav-link" href="blog/create"> | ||
| <span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> New Post | ||
| </NavLink> | ||
| </div> | ||
| </Authorized> | ||
| </AuthorizeView> | ||
|
|
||
| <AuthorizeView Roles="Admin"> | ||
| <Authorized> | ||
| <div class="nav-item px-3"> | ||
| <NavLink class="nav-link" href="admin/users"> | ||
| <span class="bi bi-people-fill-nav-menu" aria-hidden="true"></span> Manage Users | ||
| </NavLink> | ||
| </div> | ||
| </Authorized> | ||
| </AuthorizeView> | ||
|
|
||
| <AuthorizeView> | ||
| <Authorized> | ||
| <div class="nav-item px-3"> | ||
| <a class="nav-link" href="/Account/Logout"> | ||
| <span class="bi bi-box-arrow-right-nav-menu" aria-hidden="true"></span> Logout (@context.User.Identity?.Name) | ||
| </a> | ||
| </div> | ||
| </Authorized> | ||
| <NotAuthorized> | ||
| <div class="nav-item px-3"> | ||
| <a class="nav-link" href="/Account/Login"> | ||
| <span class="bi bi-box-arrow-in-right-nav-menu" aria-hidden="true"></span> Login | ||
| </a> | ||
| </div> | ||
| </NotAuthorized> | ||
| </AuthorizeView> | ||
| </nav> | ||
| </div> | ||
| <nav class="fixed top-0 left-0 right-0 z-50 | ||
| bg-primary-600 dark:bg-primary-600 | ||
| border-b-2 border-primary-200 dark:border-primary-200" | ||
| aria-label="Main navigation"> | ||
| <div class="mx-auto max-w-7xl px-4"> | ||
| <div class="flex items-center justify-between h-16"> | ||
|
|
||
| @* Brand *@ | ||
| <NavLink class="nav-link text-xl font-bold" href="" Match="NavLinkMatch.All">MyBlog</NavLink> | ||
|
|
||
| @* Mobile hamburger (pure CSS peer toggle — no JS required) *@ | ||
| <input type="checkbox" id="menu-toggle" class="peer sr-only" aria-hidden="true"/> | ||
| <label for="menu-toggle" | ||
| class="md:hidden cursor-pointer p-2 rounded hover:bg-primary-500 dark:hover:bg-primary-600 text-white" | ||
| aria-label="Toggle navigation"> | ||
| <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"> | ||
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" | ||
| d="M4 6h16M4 12h16M4 18h16"/> | ||
| </svg> | ||
| </label> | ||
|
|
||
| @* Desktop nav links *@ | ||
| <div class="hidden md:flex items-center gap-5"> | ||
|
|
||
| <NavLink class="nav-link" href="blog">Blog Posts</NavLink> | ||
|
|
||
| <AuthorizeView Roles="Author,Admin"> | ||
| <Authorized> | ||
| <NavLink class="nav-link" href="blog/create">New Post</NavLink> | ||
| </Authorized> | ||
| </AuthorizeView> | ||
|
|
||
| <AuthorizeView> | ||
| <Authorized> | ||
| <NavLink class="nav-link" href="profile">@GetProfileLabel(context.User)</NavLink> | ||
| </Authorized> | ||
| </AuthorizeView> | ||
|
|
||
| <AuthorizeView Roles="Admin"> | ||
| <Authorized> | ||
| <NavLink class="nav-link" href="admin/users">Manage Users</NavLink> | ||
| </Authorized> | ||
| </AuthorizeView> | ||
|
|
||
| @* Theme colour switcher *@ | ||
| <select @onchange="OnThemeChanged" | ||
| class="text-sm rounded border border-primary-200 dark:border-primary-800 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 px-2 py-1 cursor-pointer focus:outline-none focus:ring-2 focus:ring-primary-400" | ||
| aria-label="Colour theme" title="Select colour theme"> | ||
| <option value="red" selected="@(_currentColor == "red")">🔴 Red</option> | ||
| <option value="blue" selected="@(_currentColor == "blue")">🔵 Blue</option> | ||
| <option value="green" selected="@(_currentColor == "green")">🟢 Green</option> | ||
| <option value="yellow" selected="@(_currentColor == "yellow")">🟡 Yellow</option> | ||
| </select> | ||
|
|
||
| @* Dark mode toggle — sun icon in dark mode, moon in light *@ | ||
| <button @onclick="ToggleDark" | ||
| class="p-1.5 rounded hover:bg-primary-500 dark:hover:bg-primary-600 text-white transition" | ||
| aria-label="Toggle dark mode" title="Toggle dark mode"> | ||
| @if (_currentBrightness == "dark") | ||
| { | ||
| @* Sun — click to switch to light *@ | ||
| <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" fill="none" viewBox="0 0 24 24" | ||
| stroke="currentColor" stroke-width="2"> | ||
| <path stroke-linecap="round" stroke-linejoin="round" | ||
| d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"/> | ||
| </svg> | ||
| } | ||
| else | ||
| { | ||
| @* Moon — click to switch to dark *@ | ||
| <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" fill="none" viewBox="0 0 24 24" | ||
| stroke="currentColor" stroke-width="2"> | ||
| <path stroke-linecap="round" stroke-linejoin="round" | ||
| d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"/> | ||
| </svg> | ||
| } | ||
| </button> | ||
|
|
||
| @* Login / Logout *@ | ||
| <AuthorizeView> | ||
| <Authorized> | ||
| <a class="nav-link" href="/Account/Logout">Logout</a> | ||
| </Authorized> | ||
| <NotAuthorized> | ||
| <a class="nav-link" href="/Account/Login">Login</a> | ||
| </NotAuthorized> | ||
| </AuthorizeView> | ||
| </div> | ||
| </div> | ||
|
|
||
| @* Mobile nav (shown when peer checkbox is checked) *@ | ||
| <div class="hidden peer-checked:flex flex-col pb-4 md:hidden gap-1" | ||
| id="mobile-menu"> | ||
| <NavLink class="nav-link py-2 block" href="" Match="NavLinkMatch.All">Home</NavLink> | ||
|
|
||
| <NavLink class="nav-link py-2 block" href="blog">Blog Posts</NavLink> | ||
|
|
||
| <AuthorizeView Roles="Author,Admin"> | ||
| <Authorized> | ||
| <NavLink class="nav-link py-2 block" href="blog/create">New Post</NavLink> | ||
| </Authorized> | ||
| </AuthorizeView> | ||
|
|
||
| <AuthorizeView> | ||
| <Authorized> | ||
| <NavLink class="nav-link py-2 block" href="profile">@GetProfileLabel(context.User)</NavLink> | ||
| </Authorized> | ||
| </AuthorizeView> | ||
|
|
||
| <AuthorizeView Roles="Admin"> | ||
| <Authorized> | ||
| <NavLink class="nav-link py-2 block" href="admin/users">Manage Users</NavLink> | ||
| </Authorized> | ||
| </AuthorizeView> | ||
|
|
||
| <AuthorizeView> | ||
| <Authorized> | ||
| <a class="nav-link py-2 block" href="/Account/Logout">Logout</a> | ||
| </Authorized> | ||
| <NotAuthorized> | ||
| <a class="nav-link py-2 block" href="/Account/Login">Login</a> | ||
| </NotAuthorized> | ||
| </AuthorizeView> | ||
| </div> | ||
| </div> | ||
| </nav> | ||
|
|
||
| @code { | ||
| private string _currentColor = "blue"; | ||
| private string _currentBrightness = "light"; | ||
|
|
||
| protected override void OnInitialized() | ||
| { | ||
| Nav.LocationChanged += OnLocationChanged; | ||
| } | ||
|
|
||
| protected override async Task OnAfterRenderAsync(bool firstRender) | ||
| { | ||
| if (firstRender) | ||
| { | ||
| await SyncThemeFromJs(); | ||
| } | ||
| } | ||
|
|
||
| private async void OnLocationChanged(object? sender, LocationChangedEventArgs e) | ||
| { | ||
| try | ||
| { | ||
| await InvokeAsync(SyncThemeFromJs); | ||
|
Comment on lines
+151
to
+155
|
||
| } | ||
| catch | ||
| { | ||
| } | ||
| } | ||
|
|
||
| private async Task SyncThemeFromJs() | ||
| { | ||
| try | ||
| { | ||
| _currentColor = await Js.InvokeAsync<string>("themeManager.getColor"); | ||
| _currentBrightness = await Js.InvokeAsync<string>("themeManager.getBrightness"); | ||
| StateHasChanged(); | ||
| } | ||
| catch | ||
| { | ||
| } | ||
| } | ||
|
|
||
| private async Task OnThemeChanged(ChangeEventArgs e) | ||
| { | ||
| _currentColor = e.Value?.ToString() ?? "blue"; | ||
| await Js.InvokeVoidAsync("themeManager.setColor", _currentColor); | ||
| } | ||
|
|
||
| private async Task ToggleDark() | ||
| { | ||
| var newBrightness = _currentBrightness == "light" ? "dark" : "light"; | ||
| await Js.InvokeVoidAsync("themeManager.setBrightness", newBrightness); | ||
| _currentBrightness = await Js.InvokeAsync<string>("themeManager.getBrightness"); | ||
| StateHasChanged(); | ||
| } | ||
|
|
||
| private static string GetProfileLabel(System.Security.Claims.ClaimsPrincipal user) | ||
| { | ||
| var name = user.Identity?.Name; | ||
| return string.IsNullOrWhiteSpace(name) ? "Profile" : name; | ||
| } | ||
|
|
||
| public void Dispose() | ||
| { | ||
| Nav.LocationChanged -= OnLocationChanged; | ||
| } | ||
|
|
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The mobile nav toggle uses a visually-hidden checkbox with
aria-hidden="true"and a<label>as the interactive control. Because the checkbox is hidden from assistive technology and the label isn’t keyboard-focusable by default, this is not accessible for keyboard/screen-reader users. Consider using a real<button>witharia-expanded/aria-controls(or make the control focusable and expose state).