mitgliederverwaltung/test/mv_web/components/layouts/sidebar_test.exs
Simon 16ca4efc03
Some checks failed
continuous-integration/drone/push Build is failing
feat: implement standard-compliant sidebar with comprehensive tests
Implement a new sidebar component based on DaisyUI Drawer pattern without
custom CSS variants. The sidebar supports desktop (expanded/collapsed states)
and mobile (overlay drawer) with full accessibility compliance.

Sidebar Implementation:
- Refactor sidebar component with sidebar_header, menu_item, menu_group,
  sidebar_footer sub-components
- Add logo (mila.svg) with size-8 (32px) always visible
- Implement toggle button with icon swap (chevron-left/right) for desktop
- Add nested menu support with details/summary (expanded) and dropdown
  (collapsed) patterns
- Implement footer with language selector (expanded-only), theme toggle,
  and user menu with avatar
- Update layouts.ex to use drawer pattern with data-sidebar-expanded
  attribute for state management

CSS & JavaScript:
- Add CSS styles for sidebar state management via data-attribute selectors
- Implement SidebarState JavaScript hook for localStorage persistence
- Add smooth width transitions (w-64 ↔ w-16) for desktop collapsed state
- Add CSS classes for expanded-only, menu-label, and icon visibility

Documentation:
- Add sidebar-analysis-current-state.md: Analysis of current implementation
- Add sidebar-requirements-v2.md: Complete specification for new sidebar
- Add daisyui-drawer-pattern.md: DaisyUI pattern documentation
- Add umsetzung-sidebar.md: Step-by-step implementation guide

Testing:
- Add comprehensive component tests for all sidebar sub-components
- Add integration tests for sidebar state management and mobile drawer
- Extend accessibility tests (ARIA labels, roles, keyboard navigation)
- Add regression tests for duplicate IDs, hover effects, and tooltips
- Ensure full test coverage per specification requirements
2025-12-18 16:36:16 +01:00

850 lines
26 KiB
Elixir

defmodule MvWeb.Layouts.SidebarTest do
@moduledoc """
Unit tests for the Sidebar component.
Tests cover:
- Basic rendering and structure
- Props handling (current_user, club_name, mobile)
- Menu structure (flat and nested items)
- Footer/Profile section
- Accessibility attributes (ARIA labels, roles)
- CSS classes (DaisyUI conformance)
- Icon rendering
- Conditional visibility
"""
use MvWeb.ConnCase, async: true
import Phoenix.LiveViewTest
import MvWeb.Layouts.Sidebar
# =============================================================================
# Helper Functions
# =============================================================================
# Returns assigns for an authenticated user with all required attributes.
defp authenticated_assigns(mobile \\ false) do
%{
current_user: %{id: "user-123", email: "test@example.com"},
club_name: "Test Club",
mobile: mobile
}
end
# Returns assigns for a guest user (not authenticated).
defp guest_assigns(mobile \\ false) do
%{
current_user: nil,
club_name: "Test Club",
mobile: mobile
}
end
# Renders the sidebar component with the given assigns.
defp render_sidebar(assigns) do
render_component(&sidebar/1, assigns)
end
# Checks if the HTML contains a specific CSS class.
defp has_class?(html, class) do
html =~ class
end
# =============================================================================
# Group 1: Basic Rendering (T1.1-T1.3)
# =============================================================================
describe "basic rendering" do
test "T1.1: renders sidebar with main navigation structure" do
html = render_sidebar(authenticated_assigns())
# Check for navigation element with correct ID
assert html =~ ~s(id="main-sidebar")
assert html =~ ~s(aria-label="Main navigation")
# Check for sidebar class
assert has_class?(html, "sidebar")
end
test "T1.2: renders logo correctly" do
html = render_sidebar(authenticated_assigns())
# Check for logo image
assert html =~ ~s(src="/images/mila.svg")
assert html =~ ~s(alt="Mila Logo")
# Check logo has correct size class
assert has_class?(html, "size-8")
end
test "T1.3: renders toggle button with correct attributes (desktop only)" do
html = render_sidebar(authenticated_assigns(false))
# Check for toggle button
assert html =~ ~s(id="sidebar-toggle")
assert html =~ "onclick="
# Check for DaisyUI button classes
assert has_class?(html, "btn")
assert has_class?(html, "btn-ghost")
assert has_class?(html, "btn-sm")
assert has_class?(html, "btn-square")
# Check for both toggle icons (expanded and collapsed)
assert has_class?(html, "sidebar-expanded-icon")
assert has_class?(html, "sidebar-collapsed-icon")
end
test "T1.4: does not render toggle button on mobile" do
html = render_sidebar(authenticated_assigns(true))
# Toggle button should not be rendered on mobile
refute html =~ ~s(id="sidebar-toggle")
end
end
# =============================================================================
# Group 2: Props Handling (T2.1-T2.3)
# =============================================================================
describe "props handling" do
test "T2.1: displays club name when provided" do
assigns = %{
current_user: %{id: "user-1", email: "test@example.com"},
club_name: "My Awesome Club",
mobile: false
}
html = render_sidebar(assigns)
assert html =~ "My Awesome Club"
end
test "T2.2: does not render menu items when current_user is nil" do
html = render_sidebar(guest_assigns())
# Navigation links should not be rendered
refute html =~ ~s(href="/members")
refute html =~ ~s(href="/users")
refute html =~ ~s(href="/settings")
refute html =~ ~s(href="/contribution_types")
# Footer section should not be rendered
refute html =~ "locale-select"
refute html =~ "theme-controller"
end
test "T2.3: renders menu items when current_user is present" do
html = render_sidebar(authenticated_assigns())
# Check for Members link
assert html =~ ~s(href="/members")
# Check for Users link
assert html =~ ~s(href="/users")
# Check for Custom Fields link
assert html =~ ~s(href="/custom_fields")
# Check for Contributions section
assert html =~ ~s(href="/contribution_types")
assert html =~ ~s(href="/contribution_settings")
# Check for Settings link (placeholder)
assert html =~ ~s(href="#")
end
test "T2.4: renders sidebar with main-sidebar ID" do
html = render_sidebar(authenticated_assigns(true))
assert html =~ ~s(id="main-sidebar")
end
test "T2.5: renders sidebar with main-sidebar ID on desktop" do
html = render_sidebar(authenticated_assigns(false))
assert html =~ ~s(id="main-sidebar")
end
end
# =============================================================================
# Group 3: Menu Structure (T3.1-T3.3)
# =============================================================================
describe "menu structure" do
test "T3.1: renders flat menu items with icons and labels" do
html = render_sidebar(authenticated_assigns())
# Check for Members link with icon
assert html =~ ~s(href="/members")
assert html =~ "hero-users"
# Check for Users link with icon
assert html =~ ~s(href="/users")
assert html =~ "hero-user-circle"
# Check for Custom Fields link with icon
assert html =~ ~s(href="/custom_fields")
assert html =~ "hero-rectangle-group"
# Check for Settings link with icon
assert html =~ ~s(href="#")
assert html =~ "hero-cog-6-tooth"
# Check for tooltips (data-tip attribute)
assert html =~ "data-tip="
end
test "T3.2: renders nested menu with details element for expanded state" do
html = render_sidebar(authenticated_assigns())
# Check for Contributions section structure with details
assert html =~ "<details"
assert has_class?(html, "expanded-menu-group")
# Check for contribution links
assert html =~ ~s(href="/contribution_types")
assert html =~ ~s(href="/contribution_settings")
end
test "T3.3: renders nested menu with dropdown for collapsed state" do
html = render_sidebar(authenticated_assigns())
# Check for collapsed dropdown container
assert has_class?(html, "collapsed-menu-group")
assert has_class?(html, "dropdown")
assert has_class?(html, "dropdown-right")
# Check for dropdown-content
assert has_class?(html, "dropdown-content")
# Check for icon button
assert html =~ "hero-currency-dollar"
assert html =~ ~s(aria-haspopup="menu")
end
end
# =============================================================================
# Group 4: Footer/Profile Section (T4.1-T4.3)
# =============================================================================
describe "footer/profile section" do
test "T4.1: renders footer section when user is authenticated" do
html = render_sidebar(authenticated_assigns())
# Check for footer container with mt-auto
assert has_class?(html, "mt-auto")
# Check for language selector form
assert html =~ ~s(action="/set_locale")
# Check for theme toggle
assert has_class?(html, "theme-controller")
# Check for user menu/avatar
assert has_class?(html, "avatar")
end
test "T4.2: renders language selector with form and options" do
html = render_sidebar(authenticated_assigns())
# Check for form with correct action
assert html =~ ~s(action="/set_locale")
assert html =~ ~s(method="post")
# Check for CSRF token
assert html =~ "_csrf_token"
# Check for select element
assert html =~ ~s(name="locale")
# Check for language options
assert html =~ ~s(value="de")
assert html =~ "Deutsch"
assert html =~ ~s(value="en")
assert html =~ "English"
# Check expanded-only class
assert has_class?(html, "expanded-only")
end
test "T4.3: renders user dropdown with profile and logout links" do
assigns = %{
current_user: %{id: "user-456", email: "test@example.com"},
club_name: "Test Club",
mobile: false
}
html = render_sidebar(assigns)
# Check for dropdown container
assert has_class?(html, "dropdown")
assert has_class?(html, "dropdown-top")
# Check for avatar button
assert html =~ ~s(aria-haspopup="menu")
# Check for profile link (with user ID)
assert html =~ ~s(href="/users/user-456")
# Check for logout link
assert html =~ ~s(href="/sign-out")
# Check for DaisyUI dropdown classes
assert has_class?(html, "dropdown-content")
end
test "T4.4: renders user avatar with placeholder" do
assigns = %{
current_user: %{id: "user-789", email: "alice@example.com"},
club_name: "Test Club",
mobile: false
}
html = render_sidebar(assigns)
# Should have avatar placeholder classes
assert has_class?(html, "avatar")
assert has_class?(html, "placeholder")
assert has_class?(html, "bg-neutral")
assert has_class?(html, "text-neutral-content")
end
end
# =============================================================================
# Group 5: Accessibility Attributes (T5.1-T5.5)
# =============================================================================
describe "accessibility attributes" do
test "T5.1: navigation has correct ARIA label" do
html = render_sidebar(authenticated_assigns())
assert html =~ ~s(aria-label="Main navigation")
end
test "T5.2: toggle button has correct ARIA attributes" do
html = render_sidebar(authenticated_assigns(false))
# Toggle button
assert html =~ ~s(aria-label="Toggle sidebar")
assert html =~ ~s(aria-controls="main-sidebar")
assert html =~ ~s(aria-expanded="true")
end
test "T5.3: menu has correct roles" do
html = render_sidebar(authenticated_assigns())
# Main menu should have menubar role
assert html =~ ~s(role="menubar")
# List items should have role="none"
assert html =~ ~s(role="none")
# Links should have role="menuitem"
assert html =~ ~s(role="menuitem")
end
test "T5.4: nested menu has correct ARIA attributes" do
html = render_sidebar(authenticated_assigns())
# Details summary should have haspopup
assert html =~ ~s(aria-haspopup="true")
# Dropdown button should have haspopup
assert html =~ ~s(aria-haspopup="menu")
# Nested menus should have role="menu"
assert html =~ ~s(role="menu")
end
test "T5.5: icons are hidden from screen readers" do
html = render_sidebar(authenticated_assigns())
# Icons should have aria-hidden="true"
assert html =~ ~s(aria-hidden="true")
end
end
# =============================================================================
# Group 6: CSS Classes - DaisyUI Conformance (T6.1-T6.4)
# =============================================================================
describe "CSS classes - DaisyUI conformance" do
test "T6.1: uses correct DaisyUI menu classes" do
html = render_sidebar(authenticated_assigns())
# menu class on ul
assert has_class?(html, "menu")
end
test "T6.2: uses correct DaisyUI button classes" do
html = render_sidebar(authenticated_assigns(false))
# Button classes
assert has_class?(html, "btn")
assert has_class?(html, "btn-ghost")
assert has_class?(html, "btn-sm")
assert has_class?(html, "btn-square")
end
test "T6.3: uses correct tooltip classes" do
html = render_sidebar(authenticated_assigns())
# Tooltip classes for menu items
assert has_class?(html, "tooltip")
assert has_class?(html, "tooltip-right")
end
test "T6.4: uses correct dropdown classes" do
html = render_sidebar(authenticated_assigns())
# Dropdown classes
assert has_class?(html, "dropdown")
assert has_class?(html, "dropdown-right")
assert has_class?(html, "dropdown-top")
assert has_class?(html, "dropdown-content")
assert has_class?(html, "rounded-box")
end
end
# =============================================================================
# Group 7: Icon Rendering (T7.1-T7.2)
# =============================================================================
describe "icon rendering" do
test "T7.1: renders hero icons for menu items" do
html = render_sidebar(authenticated_assigns())
# Check for hero icons
assert html =~ "hero-users"
assert html =~ "hero-user-circle"
assert html =~ "hero-rectangle-group"
assert html =~ "hero-currency-dollar"
assert html =~ "hero-cog-6-tooth"
assert html =~ "hero-chevron-left"
assert html =~ "hero-chevron-right"
# Icons should have aria-hidden
assert html =~ ~s(aria-hidden="true")
end
test "T7.2: renders icons for theme toggle" do
html = render_sidebar(authenticated_assigns())
# Theme toggle icons (sun and moon)
assert html =~ "hero-sun"
assert html =~ "hero-moon"
end
end
# =============================================================================
# Group 8: State-dependent classes (T8.1-T8.3)
# =============================================================================
describe "state-dependent classes" do
test "T8.1: expanded-only elements have correct CSS class" do
html = render_sidebar(authenticated_assigns())
# Language form should be expanded-only
assert has_class?(html, "expanded-only")
end
test "T8.2: menu-label class is used for text that hides when collapsed" do
html = render_sidebar(authenticated_assigns())
# Menu labels that hide in collapsed state
assert has_class?(html, "menu-label")
end
test "T8.3: toggle button has state icons" do
html = render_sidebar(authenticated_assigns(false))
# Expanded icon
assert has_class?(html, "sidebar-expanded-icon")
# Collapsed icon
assert has_class?(html, "sidebar-collapsed-icon")
end
end
# =============================================================================
# Additional Edge Cases and Validation
# =============================================================================
describe "edge cases" do
test "handles user with minimal attributes" do
assigns = %{
current_user: %{id: "minimal-user", email: "min@test.com"},
club_name: "Minimal Club",
mobile: false
}
html = render_sidebar(assigns)
# Should render without error
assert html =~ "Minimal Club"
assert html =~ ~s(href="/users/minimal-user")
end
test "handles empty club name" do
assigns = %{
current_user: %{id: "user-1", email: "test@test.com"},
club_name: "",
mobile: false
}
html = render_sidebar(assigns)
# Should render without error
assert html =~ ~s(id="main-sidebar")
end
test "sidebar structure is complete with all sections" do
html = render_sidebar(authenticated_assigns())
# Header section
assert html =~ "Mila Logo"
# Navigation section
assert html =~ ~s(role="menubar")
# Footer section
assert html =~ "theme-controller"
# All expected links
expected_links = [
"/members",
"/users",
"/custom_fields",
"/contribution_types",
"/contribution_settings",
"/sign-out"
]
for link <- expected_links do
assert html =~ ~s(href="#{link}"), "Missing link: #{link}"
end
end
end
# =============================================================================
# Component Tests - sidebar_header/1
# =============================================================================
describe "sidebar_header/1" do
test "renders logo with correct size" do
html = render_sidebar(authenticated_assigns())
# Logo is size-8 (32px)
assert html =~ ~s(src="/images/mila.svg")
assert html =~ ~s(alt="Mila Logo")
assert has_class?(html, "size-8")
# Logo is always visible (no conditional classes)
assert html =~ ~s(<img)
end
test "renders club name" do
assigns = %{
current_user: %{id: "user-1", email: "test@example.com"},
club_name: "My Test Club",
mobile: false
}
html = render_sidebar(assigns)
# Club name is present
assert html =~ "My Test Club"
assert has_class?(html, "menu-label")
end
test "renders toggle button for desktop" do
html = render_sidebar(authenticated_assigns(false))
# Toggle button has both icons
assert has_class?(html, "sidebar-expanded-icon")
assert has_class?(html, "sidebar-collapsed-icon")
# Toggle button is not mobile (hidden on mobile)
assert html =~ ~s(id="sidebar-toggle")
assert html =~ ~s(aria-label="Toggle sidebar")
assert html =~ ~s(aria-expanded="true")
end
test "does not render toggle button on mobile" do
html = render_sidebar(authenticated_assigns(true))
# Toggle button should not be rendered on mobile
refute html =~ ~s(id="sidebar-toggle")
end
end
# =============================================================================
# Component Tests - menu_item/1
# =============================================================================
describe "menu_item/1" do
test "renders icon and label" do
html = render_sidebar(authenticated_assigns())
# Icon is visible
assert html =~ "hero-users"
assert html =~ ~s(aria-hidden="true")
# Label is present
assert html =~ "Members"
assert has_class?(html, "menu-label")
end
test "has tooltip with correct text" do
html = render_sidebar(authenticated_assigns())
# data-tip attribute set
assert html =~ ~s(data-tip="Members")
assert has_class?(html, "tooltip")
assert has_class?(html, "tooltip-right")
end
test "has correct link" do
html = render_sidebar(authenticated_assigns())
# navigate attribute correct (rendered as href)
assert html =~ ~s(href="/members")
assert html =~ ~s(role="menuitem")
end
end
# =============================================================================
# Component Tests - menu_group/1
# =============================================================================
describe "menu_group/1" do
test "renders expanded menu group" do
html = render_sidebar(authenticated_assigns())
# details/summary present
assert html =~ "<details"
assert html =~ "<summary"
assert has_class?(html, "expanded-menu-group")
end
test "renders collapsed menu group" do
html = render_sidebar(authenticated_assigns())
# dropdown present
assert has_class?(html, "collapsed-menu-group")
assert has_class?(html, "dropdown")
assert has_class?(html, "dropdown-right")
end
test "renders submenu items" do
html = render_sidebar(authenticated_assigns())
# Inner_block items rendered
assert html =~ ~s(href="/contribution_types")
assert html =~ ~s(href="/contribution_settings")
assert html =~ ~s(role="menu")
end
end
# =============================================================================
# Component Tests - sidebar_footer/1
# =============================================================================
describe "sidebar_footer/1" do
test "renders at bottom of sidebar" do
html = render_sidebar(authenticated_assigns())
# mt-auto present
assert has_class?(html, "mt-auto")
end
test "renders theme toggle" do
html = render_sidebar(authenticated_assigns())
# Toggle is always visible
assert has_class?(html, "theme-controller")
assert html =~ "hero-sun"
assert html =~ "hero-moon"
end
test "renders language selector in expanded only" do
html = render_sidebar(authenticated_assigns())
# expanded-only class
assert has_class?(html, "expanded-only")
assert html =~ ~s(action="/set_locale")
end
test "renders user menu with avatar" do
assigns = %{
current_user: %{id: "user-123", email: "alice@example.com"},
club_name: "Test Club",
mobile: false
}
html = render_sidebar(assigns)
# Avatar present
assert has_class?(html, "avatar")
assert has_class?(html, "placeholder")
# First letter correct (A for alice@example.com)
assert html =~ "A"
end
end
# =============================================================================
# Integration Tests - Sidebar State Management
# =============================================================================
describe "sidebar state management" do
test "sidebar starts expanded by default" do
# The data-sidebar-expanded attribute is set in layouts.ex, not in sidebar.ex
# This test verifies the sidebar structure supports the expanded state
html = render_sidebar(authenticated_assigns())
# Sidebar has classes that support expanded state
assert has_class?(html, "menu-label")
assert has_class?(html, "expanded-only")
end
test "toggle button has correct onclick handler" do
html = render_sidebar(authenticated_assigns(false))
# Toggle button has onclick handler
assert html =~ ~s(onclick="toggleSidebar()")
end
test "no duplicate IDs in layout" do
html = render_sidebar(authenticated_assigns())
# Check that main-sidebar ID appears only once
id_count = html |> String.split(~s(id="main-sidebar")) |> length() |> Kernel.-(1)
assert id_count == 1, "main-sidebar ID should appear exactly once"
# Check that sidebar-toggle ID appears only once (if present)
if html =~ ~s(id="sidebar-toggle") do
toggle_count = html |> String.split(~s(id="sidebar-toggle")) |> length() |> Kernel.-(1)
assert toggle_count == 1, "sidebar-toggle ID should appear exactly once"
end
end
end
# =============================================================================
# Integration Tests - Mobile Drawer
# =============================================================================
describe "mobile drawer" do
test "mobile header renders on small screens" do
# Mobile header is in layouts.ex, not sidebar.ex
# This test verifies sidebar works correctly with mobile flag
html = render_sidebar(authenticated_assigns(true))
# Sidebar should render without toggle button on mobile
refute html =~ ~s(id="sidebar-toggle")
end
test "drawer overlay is present" do
html = render_sidebar(authenticated_assigns())
# Drawer overlay label for mobile
assert html =~ ~s(for="mobile-drawer")
assert html =~ ~s(class="drawer-overlay")
assert html =~ ~s(lg:hidden)
end
end
# =============================================================================
# Accessibility Tests - Extended
# =============================================================================
describe "accessibility - extended" do
test "sidebar has aria-label" do
html = render_sidebar(authenticated_assigns())
assert html =~ ~s(aria-label="Main navigation")
end
test "toggle button has aria-label and aria-expanded" do
html = render_sidebar(authenticated_assigns(false))
# aria-label present
assert html =~ ~s(aria-label="Toggle sidebar")
# aria-expanded="true" or "false"
assert html =~ ~s(aria-expanded="true") || html =~ ~s(aria-expanded="false")
end
test "menu items have role attributes" do
html = render_sidebar(authenticated_assigns())
# role="menubar", "menuitem", "menu"
assert html =~ ~s(role="menubar")
assert html =~ ~s(role="menuitem")
assert html =~ ~s(role="menu")
end
test "icons have aria-hidden" do
html = render_sidebar(authenticated_assigns())
# Decorative icons: aria-hidden="true"
# Count occurrences to ensure multiple icons have it
assert html =~ ~s(aria-hidden="true")
end
test "user menu has aria-haspopup" do
html = render_sidebar(authenticated_assigns())
# aria-haspopup="menu"
assert html =~ ~s(aria-haspopup="menu")
end
end
# =============================================================================
# Regression Tests
# =============================================================================
describe "regression tests" do
test "no duplicate profile links" do
html = render_sidebar(authenticated_assigns())
# Only one Profile link in DOM
profile_count = html |> String.split(~s[href="/users/"]) |> length() |> Kernel.-(1)
assert profile_count <= 1, "Should have at most one profile link"
end
test "nested menu has only one hover effect" do
html = render_sidebar(authenticated_assigns())
# Check that menu-group has proper structure
# Both expanded and collapsed versions should be present
assert has_class?(html, "expanded-menu-group")
assert has_class?(html, "collapsed-menu-group")
# Details element should not have duplicate hover classes
# (CSS handles this, but we verify structure)
assert html =~ "<details"
end
test "tooltips only visible when collapsed" do
html = render_sidebar(authenticated_assigns())
# Tooltip classes are present (CSS handles visibility)
assert has_class?(html, "tooltip")
assert has_class?(html, "tooltip-right")
assert html =~ "data-tip="
end
test "user menu dropdown has correct structure" do
html = render_sidebar(authenticated_assigns())
# Dropdown should have proper classes
assert has_class?(html, "dropdown")
assert has_class?(html, "dropdown-top")
assert has_class?(html, "dropdown-content")
# Should have both Profile and Logout links
assert html =~ ~s(href="/sign-out")
end
end
end